> ## Documentation Index
> Fetch the complete documentation index at: https://docs.grainql.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Facts for coding agents

> One page of facts for an agent that writes code against Grain Analytics.

This page is written for an agent that writes client code. Every fact here is also on another
page of this site. When a page and the TypeScript types of the published packages disagree, the
types win.

To load this site into an agent, use the menu at the top of any page. It offers Copy, ChatGPT,
Claude, Cursor, VS Code, and MCP. Mintlify also serves the whole site as text at
`https://docs.grainql.com/llms.txt` (an index) and `https://docs.grainql.com/llms-full.txt`
(every page).

<Note>
  Machine-readable sources: `dist/index.d.ts` and `dist/types/index.d.ts` of `@grainql/tag` 4.5.1,
  and `dist/index.d.ts` plus `dist/react/index.d.ts` of `@grainql/analytics-web` 3.4.2. Read them
  from `node_modules` before you generate code.
</Note>

## Hard facts

* The tenant identifier in every call is the tenant alias that the dashboard shows at
  `https://grainql.com/dashboard`, not the tenant UUID. The placeholder on this site is
  `your-tenant-id`.
* Grain Tag is the browser script: `https://tag.grainql.com/v4/your-tenant-id.js`, or npm
  `@grainql/tag`. It collects page views, clicks, scroll depth, heatmaps, DOM snapshots, sessions,
  and attention without code. It has no remote configuration and no authentication strategy.
* Analytics Web is the TypeScript SDK: npm `@grainql/analytics-web`. It sends events and user
  properties, reads remote configuration, runs in browsers and in Node.js, and ships React hooks
  under `@grainql/analytics-web/react`.
* Both packages are cookieless by default. The visitor ID rotates every day. A persistent ID is
  stored only after a consent call.
* Analytics Web batches events. The queue flushes every 5000 milliseconds or at 50 events,
  whichever comes first. `flush()` sends the queue now.
* Analytics Web has three `authStrategy` values: `NONE` (the default), `SERVER_SIDE` with
  `secretKey`, and `JWT` with `authProvider`. `SERVER_SIDE` sends
  `Authorization: Chase <secret>`.
* The Query API base URL is `https://queryapis.grainql.com/v1/api/query`. Every request carries
  an `X-API-Key` header.
* The MCP server is at `https://grainql.com/api/mcp`. It uses OAuth 2.1 with the scopes
  `mcp:read`, `mcp:query`, and `mcp:investigate`. It is read-only.
* Every remote configuration value is a string. `getConfig` returns `string | undefined`, and
  `getAllConfigs` returns `Record<string, string>`.
* In Grain Tag, `track` and `identify` called before `init` are queued and replayed after
  `init`.
* `import { init, track, identify } from '@grainql/tag'` has no side effects. Outside a browser,
  `init` returns a no-op stub, so server-side rendering needs no guard.

## Which package

| The code must                                                                       | Use                                           |
| ----------------------------------------------------------------------------------- | --------------------------------------------- |
| Collect page views, clicks, heatmaps, and sessions without more code                | Grain Tag                                     |
| Run in a website without a build step, in Google Tag Manager, Shopify, or WordPress | Grain Tag, script tag                         |
| Run in React, Vue, Svelte, or Angular with a build step                             | Grain Tag, npm `@grainql/tag`                 |
| Read remote configuration, feature flags, or A/B variants                           | Analytics Web                                 |
| Use React hooks such as `useConfig` and `useTrack`                                  | Analytics Web, `@grainql/analytics-web/react` |
| Send events from Node.js, a serverless function, or an API route                    | Analytics Web with `SERVER_SIDE`              |
| Send events for a signed-in user with a JWT                                         | Analytics Web with `JWT`                      |
| Read events out for a dashboard or an export                                        | The Query API, no package                     |
| Let an AI assistant read analytics                                                  | The MCP server, no package                    |

Read [Install Grain](/installation) for the full comparison.

## Surface

Grain Tag, module exports of `@grainql/tag`. The script build exposes the same `init`,
`getInstance`, `track`, `identify`, and `GrainTag` on `window.GrainTag`.

| Export          | Signature                                                           |
| --------------- | ------------------------------------------------------------------- |
| `init`          | `(config?: Partial<GrainTagConfig>) => GrainTagInstance`            |
| `getInstance`   | `() => GrainTagInstance \| null`                                    |
| `isInitialized` | `() => boolean`                                                     |
| `track`         | `(eventName: string, properties?: Record<string, unknown>) => void` |
| `identify`      | `(userId: string) => void`                                          |
| `destroy`       | `() => void`                                                        |
| `GrainTag`      | `new (config?: Partial<GrainTagConfig>) => GrainTagInstance`        |

Grain Tag, methods of `GrainTagInstance`.

| Method                      | Signature                                                           |
| --------------------------- | ------------------------------------------------------------------- |
| `track`                     | `(eventName: string, properties?: Record<string, unknown>) => void` |
| `identify`                  | `(userId: string) => void`                                          |
| `consent.grant`             | `(categories?: string[]) => void`                                   |
| `consent.revoke`            | `(categories?: string[]) => void`                                   |
| `consent.status`            | `() => ConsentState \| null`                                        |
| `flush`                     | `() => Promise<void>`                                               |
| `destroy`                   | `() => void`                                                        |
| `isReady`                   | `() => boolean`                                                     |
| `updateDataLayerConnectors` | `(connectors: DataLayerConnectorConfig[]) => void`                  |

Analytics Web, the `GrainAnalytics` class of `@grainql/analytics-web`.
`createGrainAnalytics(config)` returns the same class as `new GrainAnalytics(config)`.

| Method                       | Signature                                                                                                |
| ---------------------------- | -------------------------------------------------------------------------------------------------------- |
| `track`                      | `(eventName: string, properties?: Record<string, unknown>, options?: SendEventOptions) => Promise<void>` |
| `track`                      | `(event: GrainEvent, options?: SendEventOptions) => Promise<void>`                                       |
| `identify`                   | `(userId: string) => void`                                                                               |
| `setUserId`                  | `(userId: string \| null) => void`                                                                       |
| `getUserId`                  | `() => string \| null`                                                                                   |
| `getEffectiveUserId`         | `() => string`                                                                                           |
| `getSessionId`               | `() => string`                                                                                           |
| `login`                      | `(options: { userId?: string, authToken?: string, authStrategy?: AuthStrategy }) => void`                |
| `logout`                     | `() => void`                                                                                             |
| `setProperty`                | `(properties: Record<string, unknown>, options?: { userId?: string }) => Promise<void>`                  |
| `trackLogin`                 | `(properties?: LoginEventProperties, options?: SendEventOptions) => Promise<void>`                       |
| `trackSignup`                | `(properties?: SignupEventProperties, options?: SendEventOptions) => Promise<void>`                      |
| `trackCheckout`              | `(properties?: CheckoutEventProperties, options?: SendEventOptions) => Promise<void>`                    |
| `trackPageView`              | `(properties?: PageViewEventProperties, options?: SendEventOptions) => Promise<void>`                    |
| `trackPurchase`              | `(properties?: PurchaseEventProperties, options?: SendEventOptions) => Promise<void>`                    |
| `trackSearch`                | `(properties?: SearchEventProperties, options?: SendEventOptions) => Promise<void>`                      |
| `trackAddToCart`             | `(properties?: AddToCartEventProperties, options?: SendEventOptions) => Promise<void>`                   |
| `trackRemoveFromCart`        | `(properties?: RemoveFromCartEventProperties, options?: SendEventOptions) => Promise<void>`              |
| `flush`                      | `() => Promise<void>`                                                                                    |
| `getConfig`                  | `(key: string) => string \| undefined`                                                                   |
| `getAllConfigs`              | `() => Record<string, string>`                                                                           |
| `fetchConfig`                | `(options?: RemoteConfigOptions) => Promise<RemoteConfigResponse \| null>`                               |
| `getConfigAsync`             | `(key: string, options?: RemoteConfigOptions) => Promise<string \| undefined>`                           |
| `getAllConfigsAsync`         | `(options?: RemoteConfigOptions) => Promise<Record<string, string>>`                                     |
| `preloadConfig`              | `(immediateKeys?: string[], properties?: Record<string, string>) => Promise<void>`                       |
| `addConfigChangeListener`    | `(listener: (configurations: Record<string, string>) => void) => void`                                   |
| `removeConfigChangeListener` | `(listener: (configurations: Record<string, string>) => void) => void`                                   |
| `grantConsent`               | `(categories?: string[]) => void`                                                                        |
| `revokeConsent`              | `(categories?: string[]) => void`                                                                        |
| `getConsentState`            | `() => ConsentState \| null`                                                                             |
| `hasConsent`                 | `(category?: string) => boolean`                                                                         |
| `onConsentChange`            | `(listener: (state: ConsentState) => void) => void`                                                      |
| `offConsentChange`           | `(listener: (state: ConsentState) => void) => void`                                                      |
| `destroy`                    | `() => void`                                                                                             |

`SendEventOptions` is `{ flush?: boolean }`. `RemoteConfigOptions` is
`{ immediateKeys?: string[], properties?: Record<string, string>, userId?: string, forceRefresh?: boolean, currentUrl?: string }`.
The types also export `trackSystemEvent`, `getEphemeralSessionId`, `getCurrentPage`,
`getEventCountSinceLastHeartbeat`, `resetEventCountSinceLastHeartbeat`, `getActivityDetector`,
`getEffectiveUserIdPublic`, and `log` as public methods. The tracking managers of the SDK call
them. Application code does not need them.

Analytics Web, exports of `@grainql/analytics-web/react`.

| Export                    | Signature                                                                                                                                                |
| ------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `GrainProvider`           | Component with props `{ children, client?: GrainAnalytics, config?: GrainConfig }`                                                                       |
| `useGrainAnalytics`       | `() => GrainAnalytics`                                                                                                                                   |
| `useConfig`               | `(key: string, options?: UseConfigOptions) => { value: string \| undefined, isRefreshing: boolean, error: Error \| null, refresh: () => Promise<void> }` |
| `useAllConfigs`           | `(options?: UseAllConfigsOptions) => { configs: Record<string, string>, isRefreshing: boolean, error: Error \| null, refresh: () => Promise<void> }`     |
| `useTrack`                | `() => (eventName: string, properties?: Record<string, unknown>, options?: SendEventOptions) => Promise<void>`                                           |
| `useConsent`              | `() => { consentState, grantConsent, revokeConsent, hasConsent, isGranted: boolean, categories: string[] }`                                              |
| `usePrivacyPreferences`   | `() => { preferences, updatePreferences, acceptAll, rejectAll }`                                                                                         |
| `useDataDeletion`         | `(options: { apiUrl: string, tenantId: string, onSuccess?, onError? }) => { requestDeletion, requestAnonymization, loading, error }`                     |
| `ConsentBanner`           | Component                                                                                                                                                |
| `PrivacyPreferenceCenter` | Component                                                                                                                                                |
| `CookieNotice`            | Component                                                                                                                                                |

`UseConfigOptions` and `UseAllConfigsOptions` are both
`{ forceRefresh?: boolean, immediateKeys?: string[], properties?: Record<string, string> }`.
Read [SDK reference](/api-reference/overview) and [React hooks](/react/overview).

## Configuration

`GrainTagConfig`, the argument of `init` in Grain Tag. The type file documents no defaults.

| Option            | Type                              | Default                          |
| ----------------- | --------------------------------- | -------------------------------- |
| `tenantId`        | `string`                          | Required                         |
| `apiUrl`          | `string`                          | `https://clientapis.grainql.com` |
| `debug`           | `boolean`                         | `false`                          |
| `version`         | `string`                          | set by the hosted script         |
| `consentMode`     | `'auto' \| 'opt-in' \| 'opt-out'` | `'auto'`                         |
| `enablePageViews` | `boolean`                         | `true`                           |
| `enableHeatmaps`  | `boolean`                         | `true`                           |
| `enableSnapshots` | `boolean`                         | `true`                           |
| `enableRescue`    | `boolean`                         | `true`                           |
| `enableDataLayer` | `boolean`                         | `false`                          |
| `batchSize`       | `number`                          | `50`                             |
| `flushInterval`   | `number`                          | `5000`                           |
| `retryAttempts`   | `number`                          | `3`                              |

`GrainConfig`, the argument of `new GrainAnalytics` and `createGrainAnalytics` in Analytics Web.

| Option                      | Type                                              | Default                            |
| --------------------------- | ------------------------------------------------- | ---------------------------------- |
| `tenantId`                  | `string`                                          | Required                           |
| `apiUrl`                    | `string`                                          | `'https://clientapis.grainql.com'` |
| `authStrategy`              | `'NONE' \| 'SERVER_SIDE' \| 'JWT'`                | `'NONE'`                           |
| `secretKey`                 | `string`                                          | none, required for `SERVER_SIDE`   |
| `authProvider`              | `{ getToken(): Promise<string> \| string }`       | none, required for `JWT`           |
| `userId`                    | `string`                                          | none                               |
| `batchSize`                 | `number`                                          | `50`                               |
| `flushInterval`             | `number` (milliseconds, `0` disables the timer)   | `5000`                             |
| `retryAttempts`             | `number`                                          | `3`                                |
| `retryDelay`                | `number` (milliseconds, exponential backoff)      | `1000`                             |
| `maxEventsPerRequest`       | `number`                                          | `160`                              |
| `debug`                     | `boolean`                                         | `false`                            |
| `defaultConfigurations`     | `Record<string, string>`                          | none                               |
| `configCacheKey`            | `string`                                          | `'grain_config'`                   |
| `configRefreshInterval`     | `number` (milliseconds, `0` disables the timer)   | `300000`                           |
| `enableConfigCache`         | `boolean`                                         | `true`                             |
| `consentMode`               | `'COOKIELESS' \| 'GDPR_STRICT' \| 'GDPR_OPT_OUT'` | `'COOKIELESS'`                     |
| `waitForConsent`            | `boolean`                                         | `false`                            |
| `disableAutoProperties`     | `boolean`                                         | `false`                            |
| `allowedProperties`         | `string[]`                                        | none (keep all)                    |
| `enableHeartbeat`           | `boolean`                                         | `true`                             |
| `heartbeatActiveInterval`   | `number`                                          | `120000`                           |
| `heartbeatInactiveInterval` | `number`                                          | `300000`                           |
| `enableAutoPageView`        | `boolean`                                         | `true`                             |
| `stripQueryParams`          | `boolean`                                         | `true`                             |
| `stripHash`                 | `boolean`                                         | `false`                            |
| `enableHeatmapTracking`     | `boolean`                                         | `true`                             |

Read [Configuration options](/advanced/configuration).

## Consent

| Package       | `consentMode` value | Behavior                                                            | Calls                                                                                                                                                              |
| ------------- | ------------------- | ------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Grain Tag     | `'auto'`            | The default. Cookieless, daily rotating IDs, no consent required.   | `consent.grant(categories?)`, `consent.revoke(categories?)`, `consent.status()`                                                                                    |
| Grain Tag     | `'opt-in'`          | No tracking until `consent.grant()`.                                | Same                                                                                                                                                               |
| Grain Tag     | `'opt-out'`         | Tracking by default until `consent.revoke()`.                       | Same                                                                                                                                                               |
| Analytics Web | `'COOKIELESS'`      | The default. Daily rotating IDs, no consent required.               | `grantConsent(categories?)`, `revokeConsent(categories?)`, `getConsentState()`, `hasConsent(category?)`, `onConsentChange(listener)`, `offConsentChange(listener)` |
| Analytics Web | `'GDPR_STRICT'`     | Explicit consent is required. Without it, the SDK stays cookieless. | Same                                                                                                                                                               |
| Analytics Web | `'GDPR_OPT_OUT'`    | Permanent IDs by default. Cookieless after `revokeConsent()`.       | Same                                                                                                                                                               |

A grant call moves the visitor from a daily rotating ID to a persistent ID. A revoke call clears
the persistent ID. Read [Privacy and consent](/essentials/privacy-and-compliance).

## Query API

Base URL `https://queryapis.grainql.com/v1/api/query`. Every request carries `X-API-Key` and,
for `POST`, `Content-Type: application/json`. The path parameter is the tenant alias.

| Method | Path                    | Returns                                                    |
| ------ | ----------------------- | ---------------------------------------------------------- |
| `POST` | `/{tenantAlias}`        | An array of events that match the filters, with pagination |
| `POST` | `/count/{tenantAlias}`  | `{ "count": number }`                                      |
| `GET`  | `/events/{tenantAlias}` | The distinct event names                                   |

Rate limits apply per API key.

| Plan    | Requests per minute | Requests per day | Where                   |
| ------- | ------------------- | ---------------- | ----------------------- |
| Free    | Not available       | Not available    | Every Query API request |
| Builder | 2                   | 20               | Every Query API request |
| Growth  | 10                  | 200              | Every Query API request |
| Scale   | 100                 | 2,000            | Every Query API request |

A response over the limit is `429` with a `Retry-After` header in seconds. Custom plans with
higher limits exist. Read [Query API](/api-reference/query-api/overview) for the error responses
and [Export and query data](/guides/query-api) for a task guide.

## Mistakes to avoid

<AccordionGroup>
  <Accordion title="Using the tenant UUID instead of the tenant alias">
    Every SDK, the script URL, and the Query API path take the tenant alias that the dashboard
    shows. A UUID in `tenantId` sends events to no tenant. Replace `your-tenant-id` with the alias.
  </Accordion>

  <Accordion title="Putting a SERVER_SIDE secret in browser code">
    `secretKey` is a server credential. A bundle ships it to every visitor. In the browser, use
    Grain Tag, or Analytics Web with `NONE` or `JWT`. Keep `SERVER_SIDE` in Node.js and read the
    key from `$GRAIN_SECRET_KEY`.
  </Accordion>

  <Accordion title="Expecting remote configuration from Grain Tag">
    `GrainTagInstance` has no `getConfig`. Remote configuration, feature flags, and A/B variants
    are Analytics Web features. Install `@grainql/analytics-web` for them.
  </Accordion>

  <Accordion title="Expecting heatmaps from Analytics Web without enableHeatmapTracking">
    Analytics Web collects no heatmap data by default. Set `enableHeatmapTracking: true`, or use
    Grain Tag, which collects heatmaps and DOM snapshots without an option.
  </Accordion>

  <Accordion title="Mixing the two SDK APIs in one file">
    Grain Tag has `init`, `track`, `identify`, and `consent.grant`. Analytics Web has
    `createGrainAnalytics`, `grain.track`, `grain.login`, and `grain.grantConsent`. A file that
    imports both sends every event two times. Pick one package per surface.
  </Accordion>

  <Accordion title="Forgetting flush() before a serverless function exits">
    Analytics Web queues events and sends them on a timer. A function that returns before the
    timer fires loses the queue. Call `await grain.flush()` before the return, or pass
    `{ flush: true }` to `track`.
  </Accordion>

  <Accordion title="Treating a configuration value as a boolean">
    Every configuration value is a string. `getConfig('feature_enabled')` returns `'true'`, not
    `true`. Compare with `=== 'true'`.
  </Accordion>

  <Accordion title="Retrying on 429 without reading Retry-After">
    The Query API allows 2 requests per minute on the Builder plan. A tight retry loop stays at
    `429`. Read the `Retry-After` header and wait that many seconds before the next request.
  </Accordion>
</AccordionGroup>

## Client checklist

<Steps>
  <Step title="Read the tenant alias from the dashboard">
    Put it in `tenantId`, in the script URL, or in the Query API path. Never the UUID.
  </Step>

  <Step title="Pick one package per surface">
    Grain Tag in the browser for automatic tracking. Analytics Web for remote configuration, React
    hooks, and servers.
  </Step>

  <Step title="Keep secrets on the server">
    `secretKey` and the Query API key come from environment variables in server code only.
  </Step>

  <Step title="Name events in snake_case">
    Use one noun and one past-tense verb, for example `signup_completed`. Read
    [Name events](/core/event-naming).
  </Step>

  <Step title="Call identify or login after sign-in">
    Before that call, the visitor has a daily rotating ID. Read
    [Identify users](/core/user-identification).
  </Step>

  <Step title="Flush before a short-lived process exits">
    `await grain.flush()` at the end of a serverless function, a script, or a test.
  </Step>

  <Step title="Compare configuration values as strings">
    Give every key a default in `defaultConfigurations` so the first render has a value.
  </Step>
</Steps>

## Do not generate

* Do not generate a consent banner that blocks tracking in the default mode. Both packages are
  cookieless by default and need no consent for that mode.
* Do not generate code that reads `window.grain`. The script build exposes `window.GrainTag`.
  Analytics Web exposes nothing on `window` when it is imported from npm.
* Do not generate an `apiUrl` other than the default unless the user has one.
* Do not generate a call to `grain.getConfig` in Grain Tag code, or a call to `consent.grant` in
  Analytics Web code. Each package has its own API.
* Do not generate a Query API call from browser code. The API key is a server credential.
* Do not generate a method, an option, or an endpoint that is not in the tables on this page.
