Events and identity
This page covers the tracking API of the analytics instance: the standard
events and how each provider translates them, per-event options, user
identity and groups, and reading and changing consent from code. The instance
is the object returned by createAnalytics(), or by useAnalytics() in React.
Send every event through the instance rather than calling gtag, fbq or
another vendor global directly. The instance delivers each call only to the
providers whose consent requirement is met at that moment, and each provider
translates the call into its vendor’s format. Provider failures never reject
the returned promise; they are reported to the onError handler passed to
createAnalytics().
Standard events
Section titled “Standard events”analytics.track(name, params?, options?) accepts any event name. Names
follow GA4’s conventions. The names below are declared in the
StandardEvents interface, so their params are type-checked; any other name
is sent as a custom event with free-form params (Record<string, unknown>).
Standard events also accept additional params beyond their declared ones.
| Use | Events |
|---|---|
| Any app | sign_up, login, search, lead, share, contact |
| Ecommerce | view_item, view_item_list, add_to_cart, remove_from_cart, add_to_wishlist, begin_checkout, add_payment_info, add_shipping_info, purchase, refund |
| SaaS | trial_started, subscription_started, subscription_cancelled |
analytics.track('sign_up', { method: 'google' })analytics.track('search', { search_term: 'boots' })analytics.track('purchase', { transaction_id: 'o_123', value: 49.9, currency: 'EUR', items: [{ item_id: 'sku_1', item_name: 'Boots', price: 49.9, quantity: 1 }],})Parameter types of the standard events
search_term on search and transaction_id on purchase and refund are
required; every other param is optional.
type Money = { value?: number; currency?: string }type EcommerceParams = Money & { items?: EcommerceItem[] }
type EcommerceItem = { item_id: string item_name?: string item_variant?: string item_brand?: string item_category?: string price?: number quantity?: number discount?: number coupon?: string}
interface StandardEvents { sign_up: { method?: string } login: { method?: string } search: { search_term: string } lead: Money share: { method?: string; content_type?: string; item_id?: string } contact: { method?: string } view_item: EcommerceParams view_item_list: EcommerceParams & { item_list_id?: string; item_list_name?: string } add_to_cart: EcommerceParams remove_from_cart: EcommerceParams add_to_wishlist: EcommerceParams begin_checkout: EcommerceParams & { coupon?: string } add_payment_info: EcommerceParams & { payment_type?: string } add_shipping_info: EcommerceParams & { shipping_tier?: string } purchase: EcommerceParams & { transaction_id: string; tax?: number; shipping?: number; coupon?: string } refund: EcommerceParams & { transaction_id: string } trial_started: Money & { plan?: string } subscription_started: Money & { plan?: string } subscription_cancelled: { plan?: string; reason?: string }}Ecommerce helpers
Section titled “Ecommerce helpers”Ecommerce events carry GA4’s items[] array. consentlayer/ecommerce builds
these params from the product, cart and order shapes most storefront APIs
return, with Shopify-style { amount, currencyCode } prices.
import { cartParams, orderParams, productParams } from 'consentlayer/ecommerce'
analytics.track('view_item', productParams(product))analytics.track('add_to_cart', productParams({ ...product, quantity: 2 }))analytics.track('begin_checkout', cartParams(cart))analytics.track('purchase', orderParams(order))| Helper | For | value |
currency |
|---|---|---|---|
productParams(product) |
view_item, add_to_cart, remove_from_cart, add_to_wishlist |
price.amount × quantity (quantity defaults to 1); omitted without a price |
price.currencyCode |
cartParams(cart) |
begin_checkout, add_payment_info, add_shipping_info |
subtotal.amount, or the sum of price × quantity over the lines |
subtotal.currencyCode, else the first line’s |
orderParams(order) |
purchase |
order.value |
order.currency |
orderParams also maps orderId to transaction_id and passes tax,
shipping and coupon through.
Input types and the item mapping
type MoneyLike = { amount: number | string; currencyCode: string }
type ProductRef = { id: string variantId?: string title?: string vendor?: string category?: string | null price?: MoneyLike quantity?: number}
type CartRef = { subtotal?: MoneyLike; lines: ProductRef[] }
type OrderRef = { orderId: string value: number currency: string tax?: number shipping?: number coupon?: string | null lines: ProductRef[]}Each ProductRef becomes one item: id → item_id, title → item_name,
variantId → item_variant, vendor → item_brand, category →
item_category, Number(price.amount) → price and quantity →
quantity, which defaults to 1. toItem(product) performs this conversion on
its own.
What each provider receives
Section titled “What each provider receives”GA4, Meta and TikTok rename standard events to their own vocabulary. Every
other provider receives the name you passed to track().
| Standard event | GA4 | Meta | TikTok |
|---|---|---|---|
view_item |
view_item |
ViewContent |
ViewContent |
add_to_cart |
add_to_cart |
AddToCart |
AddToCart |
add_to_wishlist |
add_to_wishlist |
AddToWishlist |
AddToWishlist |
begin_checkout |
begin_checkout |
InitiateCheckout |
InitiateCheckout |
add_payment_info |
add_payment_info |
AddPaymentInfo |
AddPaymentInfo |
purchase |
purchase |
Purchase |
Purchase |
sign_up |
sign_up |
CompleteRegistration |
CompleteRegistration |
lead |
generate_lead |
Lead |
Lead |
search |
search |
Search |
Search |
contact |
contact |
Contact |
Contact |
trial_started |
trial_started |
StartTrial |
StartTrial |
subscription_started |
subscription_started |
Subscribe |
Subscribe |
customize_product, schedule, submit_application, find_location, donate |
unchanged | CustomizeProduct, Schedule, SubmitApplication, FindLocation, Donate |
CustomizeProduct, Schedule, SubmitApplication, FindLocation; donate unchanged |
| Any other name | Characters other than letters, digits and _ replaced with _ |
Sent with fbq('track') if it is already one of Meta’s standard event names, otherwise not sent unless listed in events, then with fbq('trackCustom') under the original name |
Not sent unless listed in events, then unchanged, as a custom event |
The params each provider sends differ as follows:
| Provider | Call | Params |
|---|---|---|
| GA4 | gtag('event', name, params) |
The event params, merged with providers.ga4 |
| GTM | dataLayer.push({ ...params, event: name }) |
The event params, merged with providers.gtm, pushed with the unchanged name as event |
| Meta Pixel | fbq('track' | 'trackCustom', name, params, { eventID }) |
Translated as described below, merged with providers.meta |
| TikTok Pixel | ttq.track(name, params, { event_id }) |
Translated as described below, merged with providers.tiktok |
| PostHog | posthog.capture(name, params) |
The event params, merged with providers.posthog |
| Vercel | The trackEvent function you pass |
Top-level strings, numbers, booleans and null; undefined becomes null, a Date becomes an ISO string, and every other value is dropped |
| Contentsquare | _uxa.push(['trackPageEvent', name]) |
None. purchase instead creates and sends an ecommerce transaction with transaction_id, value and currency as id, revenue and currency |
| Hotjar | hj('event', name) |
None |
Sentry receives no events, only errors from captureError().
Meta receives items[] as content_ids, contents (id, quantity,
item_price), content_type: 'product' and num_items, the sum of the item
quantities. Each item’s ID is its item_variant when present and its
item_id otherwise, and a missing quantity counts as 1. search_term is
renamed to search_string. Every other param is passed through.
TikTok receives items[] as content_ids, contents (content_id,
content_name, content_category, brand, price and quantity, from
item_name, item_category, item_brand, price and quantity),
content_type: 'product' and num_items, with the same item ID and quantity
rules as Meta. search_term is renamed to search_string and
transaction_id to order_id. Every other param, such as value and
currency, is passed through.
Consent
Section titled “Consent”An event reaches a provider only while the provider’s consent requirement is
met: analytics for GA4 (none with consentMode: 'advanced'), Contentsquare and
Hotjar, and marketing for Meta and TikTok. Vercel and Sentry require no
consent, and neither does Google Tag Manager unless you pass
requiredConsent. PostHog requires none in its
default cookieless mode, and analytics with cookieless: false; PostHog and
Sentry add cookies, session recording and user data only once analytics is
granted. Providers documents each provider’s options
and provider internals the exact calls.
When server.endpoint is set, each event is also posted once to that route
while analytics or marketing is granted, with its ID, name, params, URL and
the only and providers options. See
server-side tracking.
Routing events
Section titled “Routing events”Each provider decides which events it receives with its events option.
Analytics providers, such as GA4, GTM and PostHog, receive every event by
default. The Meta and TikTok pixels, and their server destinations, receive
only the events that map to one of their
standard events.
createAnalytics({ providers: [ ga4({ measurementId }), meta({ pixelId, events: ['purchase', 'quote_requested'] }), hotjar({ siteId, version, events: false }), ],})A list sends exactly those names, and false sends none; a provider’s page
views are not affected. A server destination’s events also covers
page_view; see Server-side tracking. To send a single event elsewhere, name the providers in its
only option. only wins over events: the event goes to
every provider it names and to no other.
Typed custom events
Section titled “Typed custom events”Declare your own events in the file that creates the instance. Their params are then type-checked like those of the standard events:
export const analytics = createAnalytics({ providers })
declare module 'consentlayer' { interface Register { events: { quote_requested: { value: number; currency: string } filter_changed: { filter: string } } }}Once Register declares events, track() accepts only the standard events,
the declared events and the auto-tracking events, so
a misspelled name is a type error. The events option of every provider is
checked against the same names. Without the declaration, track() accepts any
name.
Event options
Section titled “Event options”The third argument of track() scopes a single event.
analytics.track('purchase', params, { eventId: order.id, only: ['ga4', 'meta'], providers: { meta: { content_category: 'shoes' } }, requireMarketingConsent: true,})| Option | Type | Behavior |
|---|---|---|
eventId |
string |
ID shared by the browser and server events so destinations can deduplicate them; Meta receives it as eventID; TikTok, the Conversions API and the Events API as event_id. Defaults to crypto.randomUUID(). |
only |
string[] |
Provider IDs that receive the event, whatever their events option says. Other providers skip it. |
providers |
Record<string, Record<string, unknown>> |
Extra params for one provider, keyed by provider ID and merged over the event params. Read by the GA4, GTM, Meta, TikTok and PostHog providers, and on the server by Meta CAPI and the TikTok Events API; ga4Server and sgtm read providers.ga4. |
requireAnalyticsConsent |
boolean |
Drops the event, for providers and the server endpoint alike, unless analytics is granted when it is delivered. |
requireMarketingConsent |
boolean |
The same, for marketing. |
Identity
Section titled “Identity”identify(userId?, traits?) records who the visitor is, and reset() forgets
it.
await analytics.identify('user_123', { email: 'ada@example.com', plan: 'pro' })await analytics.reset()- User ID. Converted to a string and passed to every active provider that
accepts one; a provider is active only while its own consent requirement is
met. Identifying a different user ID first
discards the current identity, including email and phone. Calling
identify(undefined, traits)keeps the current user ID. - Email and phone. Read from
traits.emailandtraits.phone. Email is trimmed and lowercased; phone loses spaces, hyphens, parentheses and dots and must then be an E.164 number. Invalid values are dropped silently. Providers receive them only while marketing is granted. Until then they are held in memory until the next consent change, which shares them if it grants marketing and discards them if it doesn’t. Withdrawing marketing discards them: active providers are identified again without them, and providers no longer active are reset. - Other traits. Passed to the providers’
identifycalls for this call only; they are not stored. - Storage. The identity is written to localStorage under
consentlayer_user_identity(see Names) only while analytics or marketing is granted, and removed otherwise. On the next page load the user ID is restored, and the stored email and phone are restored only if marketing is granted and the stored user ID still matches. - Late providers. A provider that becomes active later, for example when its consent is granted, is identified with the current identity when it starts.
- Before boot.
track,page,identify,groupandcaptureErrorcalls made beforeboot()completes are queued and delivered in order, right after boot, to the providers consent allows at that moment. The queue holds 100 calls; later ones are dropped and a singlequeue-fullerror is reported toonError.
| Provider | User ID | Email and phone (marketing granted) |
|---|---|---|
| GA4 | gtag('set', { user_id }) |
gtag('set', 'user_data', { email, phone_number }). Requires user-provided data collection to be enabled in GA4 admin. |
| Meta Pixel | external_id in advanced matching |
em and ph in advanced matching. The pixel accepts these only in fbq('init'), so it receives the identity known when it loads, usually none on a first visit. |
| Meta Conversions API | external_id, SHA-256 hashed, on every event |
em and ph, SHA-256 hashed, on every event |
| TikTok Pixel | external_id in ttq.identify |
email and phone_number in ttq.identify |
| TikTok Events API | external_id, SHA-256 hashed, on every event |
email and phone, SHA-256 hashed, on every event |
| GTM | consentlayer_identify event with user_id, while analytics is granted |
Not sent |
| PostHog | posthog.identify(userId, traits), while analytics is granted |
email added to the person properties |
| Sentry | Sentry.setUser({ id }), while analytics is granted |
email |
| Contentsquare | identify |
Not sent |
| Hotjar | hj('identify', userId, traits) |
email as an attribute, only with a user ID |
| Vercel | Not sent | Not sent |
Logging out
Section titled “Logging out”reset() clears the identity in memory and in storage, then calls reset on
every provider that has been set up, whatever the current consent:
| Provider | Effect of reset() |
|---|---|
| GA4 | Sets user_id to null and user_data to {} |
| PostHog | Calls posthog.reset() and reapplies the current consent |
| Sentry | Calls Sentry.setUser(null) |
| Contentsquare | Sends resetIdentity |
| Hotjar | Identifies as anonymous with hj('identify', null, {}) |
| GTM | Pushes a consentlayer_identify event with user_id: null, if a user ID was pushed before |
| Meta Pixel | No effect |
| TikTok Pixel | No effect |
| Vercel | No effect |
Three vendors cannot fully forget a user without a page load:
- The Meta pixel keeps the advanced-matching data from
fbq('init')until the page is reloaded. If the pixel was initialized with an email or phone and marketing is then withdrawn, the pixel stays revoked for the rest of the page, even if marketing is granted again. - The TikTok pixel keeps what
ttq.identifyreceived until the page is reloaded, because ConsentLayer’s TikTok provider has noresethook. If the pixel received an email or phone and marketing is then withdrawn, it stays revoked for the rest of the page, even if marketing is granted again. - Hotjar has no API to remove the email attribute or to log out. It keeps linking the browser to the last user until its cookies are cleared, which ConsentLayer does when analytics consent is withdrawn.
Groups
Section titled “Groups”analytics.group(groupId, traits?) is queued and delivered like track(), to
the providers that implement groups. Among the built-in providers only PostHog
does, with posthog.group(groupType, groupId, traits) while analytics is
granted; groupType is a PostHog option that defaults to company.
Consent
Section titled “Consent”analytics.consent reads and changes the visitor’s choice.
await analytics.consent.update({ analytics: true })const consent = analytics.consent.getConsent()const unsubscribe = analytics.consent.subscribe(({ consent }) => { console.log(consent?.marketing)})| Member | Behavior |
|---|---|
categories |
Every category the visitor can toggle: preferences, analytics and marketing, followed by your custom categories. |
getConsent() |
The current ConsentState, or null until the visitor has chosen. |
isStale() |
Whether the stored choice is too old or was made under another policy version, so the visitor is asked again. See Asking again. |
update(partial) |
Waits for bootstrap(), then merges partial into the current state. Categories left out keep their value; unset categories are denied. |
setConsent(consent) |
Replaces the whole state. Throws a TypeError ([consentlayer] consent must have boolean preferences, analytics and marketing, received …) unless every category is a boolean. |
bootstrap() |
Reads the stored consent, or asks the consent adapters when nothing is stored. Runs once; later calls return the same promise. <AnalyticsProvider> calls it for you. |
subscribe(listener) |
Calls listener({ consent }) after every change and returns an unsubscribe function. |
A change that leaves the state unchanged is ignored. update() and
setConsent() store the new state, broadcast it to other scripts (see
Names), notify subscribers and providers, and then send it
to the consent adapters and the server.consentEndpoint, resolving once they
have finished. A change read from another tab is broadcast and applied but not
stored or sent again.
In React, useConsent() returns { consent, update, setConsent, engine },
where engine is analytics.consent, and re-renders when consent changes.
Like useAnalytics(), it works outside <AnalyticsProvider> by using the
instance the most recent createAnalytics() call created. To build a banner,
see Consent UI.