npm package: @grainql/tag
Description: Grain Tag - Lightweight analytics SDK for Grain
Author: Grain Analytics
Latest version: 4.4.6

<br>
<p align="center">
  <img src="https://cdn.xreos.co/webasset/images/grain_logomark_dark.svg" alt="Grain" width="150"/>
</p>

<h1 align="center">Grain Tag</h1>

<p align="center">
  Privacy-first analytics SDK for <a href="https://grainql.com">Grain</a>.<br/>
  Works everywhere — script tag, npm, any framework, SSR or CSR.
</p>

<p align="center">
  <a href="https://www.npmjs.com/package/@grainql/tag"><img src="https://img.shields.io/npm/v/@grainql/tag" alt="npm version" /></a>
  <a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="License" /></a>
</p>

---

## Installation

### Script Tag (via Cloudflare Workers)

```html
<script src="https://tag.grainql.com/v4/your-tenant-id.js"></script>
```

The SDK auto-initializes with your tenant config injected at the edge. No setup needed.

### npm

```bash
npm install @grainql/tag
```

## Quick Start

### Script Tag

```html
<script src="https://tag.grainql.com/v4/your-tenant-id.js"></script>
<script>
  // Auto-initialized — start tracking immediately
  GrainTag.track('button_clicked', { button: 'signup' });

  // Identify a user after login
  GrainTag.identify('user-123');
</script>
```

### npm (React, Next.js, Vue, Svelte, etc.)

```typescript
import { init, track, identify } from '@grainql/tag';

// Initialize once
init({ tenantId: 'your-tenant-id' });

// Track events — even before init(), events are queued and replayed
track('page_viewed', { page: '/home' });

// Identify after login
identify('user-123');
```

### SSR / Server-Side

```typescript
import { init, track } from '@grainql/tag';

// Safe to call on the server — returns a no-op stub, never crashes
const grain = init({ tenantId: 'your-tenant-id' });
track('page_viewed'); // silently ignored on server, works on client
```

## Configuration

```typescript
init({
  tenantId: 'your-tenant-id',        // Required
  apiUrl: 'https://clientapis.grainql.com', // Default
  debug: false,                       // Enable debug logging
  consentMode: 'auto',               // 'auto' | 'opt-in' | 'opt-out'
  enablePageViews: true,             // Automatic page view tracking
  enableHeatmaps: true,              // Click and scroll heatmaps
  enableSnapshots: true,             // DOM snapshots for visual overlays
  enableRescue: true,                // Exit-intent rescue popups
  batchSize: 50,                     // Events per batch
  flushInterval: 5000,               // Flush interval in ms
  retryAttempts: 3,                  // Retry attempts on failure
});
```

## API Reference

### Event Tracking

```typescript
track(eventName, properties?)        // Track a custom event
```

### Identity

```typescript
identify(userId)                     // Set user identity
```

### Consent

```typescript
grain.consent.grant(['analytics'])   // Grant consent for categories
grain.consent.revoke()               // Revoke consent
grain.consent.status()               // Get current consent state
```

### Lifecycle

```typescript
getInstance()                        // Get the current SDK instance
isInitialized()                      // Check if SDK is initialized
destroy()                            // Tear down and allow re-init
grain.flush()                        // Flush pending events
grain.isReady()                      // True if running in browser
```

## Consent Modes

| Mode | Default Behavior | On Consent |
|------|-----------------|------------|
| `auto` | Cookieless (daily rotating IDs) | Upgrades to permanent IDs |
| `opt-in` | Cookieless until explicit consent | Upgrades to permanent IDs |
| `opt-out` | Permanent IDs by default | Downgrades to cookieless on revoke |

## How It Works

Events flow through a simple pipeline:

1. `track()` enqueues events into an in-memory batch — it never blocks.
2. Every 5 seconds (or when the batch reaches 50 events), events are flushed to the Grain API.
3. Failed requests are retried with exponential backoff up to 3 times.
4. On page unload or tab switch, remaining events are delivered via the Beacon API for reliable last-mile delivery.
5. User identity uses daily rotating IDs by default (cookieless). On consent, it upgrades to persistent IDs stored in localStorage.

The SDK automatically attaches `device`, `browser`, `os`, UTM parameters, first-touch attribution, and session data to every event.

## Building from Source

```bash
npm run build:sdk       # Build SDK (IIFE, ESM, CJS + type declarations)
npm run build:worker    # Build Cloudflare Worker
npm run build           # Build everything
npm run build:dev       # Development build (unminified)
npm run typecheck       # TypeScript type checking
```

## Grain SDKs

| Use Case | SDK |
|----------|-----|
| Browser analytics, heatmaps, snapshots | `@grainql/tag` (this package) |
| Script tag delivery (Cloudflare Workers) | `@grainql/tag` (this package) |
| Remote configuration | [`@grainql/analytics-web`](https://www.npmjs.com/package/@grainql/analytics-web) |
| Server-side event tracking (Node.js) | [`@grainql/analytics-web`](https://www.npmjs.com/package/@grainql/analytics-web) |

### Migrating from `@grainql/analytics-web`

If you were using `@grainql/analytics-web` for browser analytics:

```typescript
// Before
import { createGrainAnalytics } from '@grainql/analytics-web';
const grain = createGrainAnalytics({ tenantId: '...' });
grain.track('event');

// After
import { init, track } from '@grainql/tag';
init({ tenantId: '...' });
track('event');
```

- `createGrainAnalytics()` → `init()`
- `grain.setUserId()` → `identify()`
- Consent API: `grain.grantConsent()` → `grain.consent.grant()`

## Contributing

See [CONTRIBUTING.md](CONTRIBUTING.md).

## License

[MIT](LICENSE)