@crelora/mark
v0.2.1
Published
Mark is Crelora's lightweight attribution SDK for capturing user journeys, conversions, and consent across browsers and server-side runtimes. The npm package includes both browser and Node entry points so you can send consistent data from any surface.
Downloads
303
Readme
Crelora Mark SDK
Mark is Crelora's lightweight attribution SDK for capturing user journeys, conversions, and consent across browsers and server-side runtimes. The npm package includes both browser and Node entry points so you can send consistent data from any surface.
Use it to feed OneLence with first‑party behavioral and conversion data so you can build analytics, insights, signals, and decisions on top of a unified event stream.
API keys and documentation
- Keys: Publishable keys (
pk_…) for browser and secret keys (sk_…) for server-side use are available in the OneLence dashboard: API keys. - Guides: For integration patterns, alternative integration types (e.g. server-only, tag managers), and deeper technical documentation, see the Integrations overview.
Installation
npm install @crelora/mark
# or
yarn add @crelora/markRuntime Imports
- Browser runtime:
import { Mark } from '@crelora/mark' - Node runtime:
import { createNodeMark } from '@crelora/mark/node'
CDN / Script Tag Usage
You can also drop the SDK directly into your browser applications via a script tag. This exposes the global window.Mark object.
<!-- Use a CDN like unpkg or jsdelivr -->
<script src="https://unpkg.com/@crelora/mark@latest/dist/browser.umd.js"></script>
<script>
// Mark is available globally
Mark.init({
key: 'pk_xxxxx',
// ... configuration
});
Mark.track('Page View');
</script>Browser Quickstart
import { Mark } from '@crelora/mark';
Mark.init({
key: 'pk_xxxxx',
require_consent: 'auto',
cross_domain: { cookie_domain: '.example.com' },
site_id: 'uuid-...', // Optional: associate events with a registered site
site_host: 'shop.example.com', // Optional: site host for audit/debug
autocapture: { pageview: true }, // Optional: auto-emit page_view on load and SPA route changes
});
Mark.identify('user_123', {
email: '[email protected]',
display_name: 'John Doe',
language: 'en-US'
});
Mark.track('Checkout Started', { value: 12900, currency: 'usd' });
Mark.setConsent('granted');
// Per-event site overrides (optional)
Mark.track('Conversion', {
site_id: 'different-site-uuid', // Overrides init config for this event
site_host: 'other.example.com',
value: 5000,
});Mark.init should be called once during app bootstrap. Subsequent calls to track, identify, or setConsent reuse the same client instance.
Node / Server Usage
import { createNodeMark } from '@crelora/mark/node';
const mark = createNodeMark({
key: process.env.MARK_SECRET_KEY!,
site_id: process.env.MARK_SITE_ID, // Optional: associate events with a registered site
site_host: process.env.MARK_SITE_HOST, // Optional: site host for audit/debug
});
mark.track('Server Conversion', {
visitor_id: 'vis_abc',
order_id: 'ord_789',
value: 19900,
});The Node factory accepts optional custom storage or transport adapters so you can plug the SDK into queues or serverless environments.
Available Methods
Mark.init(config)/createNodeMark(config)– bootstraps the client with your publishable or secret key.track(eventName, payload?)– records custom events with arbitrary properties (numbers, strings, arrays, objects). Usesite_idandsite_hostfor per-event site overrides.conversion(eventName, payload?)– records conversion events (same endpoint path as track, withis_conversion: truefor backend compatibility).identify(userId, traits?)– ties a known user identifier to previous anonymous activity. Recommended traits:email,display_name,language. Custom traits are also supported.setConsent(status)– enforces consent gating; pass'granted'or'denied'.getVisitorId()– returns the current pseudonymous visitor ID when available. On the browser, returnsundefineduntil consent is granted whenrequire_consentis set. Use it to send the ID to your backend (e.g. in a header or body) for server-side attribution when you don't have an authenticated user ID.flush()– flushes queued/persisted delivery items.reset()– clears user/session/attribution state and rotates visitor identity for logout flows.getStats()– returns runtime delivery stats{ queued, sent, failed, dropped }.setInternal(value)/getInternal()– flags the current visitor as internal traffic (QA, staff, smoke tests). While set, every outgoing event is stamped withis_internal: trueso the backend can exclude it from customer-facing reports by default. See Flagging internal traffic.
Reserved SDK fields (for example event_name, user_id, consent_state, and internal identity metadata) are sanitized from user payloads/traits and cannot override SDK-managed values.
Automatic Attribution Tracking
The SDK automatically captures and persists the following URL parameters:
- UTM keys:
utm_source,utm_medium,utm_campaign,utm_term,utm_content - Referral aliases:
ref,referral,affiliate_id(normalized toref) - Generic IDs:
click_id,ch_click_id,cid,campaign_id - Paid platform IDs:
gclid,gbraid,wbraid,dclid,msclkid,fbclid,ttclid,twclid,li_fat_id
These parameters are included in every tracked event to ensure proper attribution.
Capture is evaluated at SDK initialization and refreshed before each auto page view emit when autocapture.pageview + route tracking are enabled.
- If consent is not yet granted (
require_consent: trueor'auto'), attribution is kept in runtime memory and not persisted. - After consent is granted, pending in-memory attribution and current URL attribution are persisted.
Custom Query Param Capture
You can expand the default attribution allowlist or opt into full query capture:
Mark.init({
key: 'pk_xxxxx',
capture_query_params: ['sub_id1', 'sub_id2', 'publisher_code'],
// Optional:
// capture_all_query_params: true,
// query_param_denylist: ['email', 'token', 'auth'],
// max_captured_query_params: 30,
// max_query_param_value_length: 256,
});Configuration Reference
All config options use snake_case. Stored event payloads and database columns match 1:1.
| Option | Type | Description |
| --- | --- | --- |
| key | string | Publishable (browser) or secret (server) key from OneLence API keys. |
| debug | boolean | Enables verbose console logging to help with integration tests. |
| before_send | (event) => event \| null | Mutate/redact payloads before send, or return null to drop events. |
| on_error | (error, event?) => void | Hook for transport and queue failures. |
| sample_rate | number | Fraction (0..1) for track event sampling. identify and conversion are never sampled. |
| honor_dnt | boolean | When true, blocks tracking when browser DNT/GPC is enabled. |
| session_timeout_ms | number | Inactivity window for rotating session_id (default 30 minutes). |
| request_timeout_ms | number | HTTP timeout per request in milliseconds (default 10000). |
| rotate_visitor_on_consent_change | boolean | Rotate visitor_id after denied -> granted transition. |
| batching | { enabled?: boolean, max_size?: number, flush_interval_ms?: number, endpoint_path?: string } | Optional batch mode (/events by default). |
| require_consent | boolean \| 'auto' | true blocks tracking until consent is granted, 'auto' requires stored granted consent and treats missing consent as denied, default false ('auto' recommended for production). |
| consent_source | { type: 'tcf', purposes: number[] } | Optional IAB TCF v2 integration: the SDK listens for CMP updates and only allows tracking when the listed numeric purpose IDs are consented. Combine with require_consent and setConsent as your legal team requires. |
| autocapture | { pageview?: boolean, click?: boolean \| { selector?: string }, form_submit?: boolean, outbound_link?: boolean, scroll_depth?: boolean, web_vitals?: boolean } | Auto-capture toggles for page views and optional interaction/perf signals. |
| track_route_changes | boolean | When autocapture.pageview is true, also emits on SPA route changes (pushState/replaceState/popstate); defaults to true. |
| include_page_context | boolean | When true (default), enriches events with page, title, referrer, site (full url is only sent if you pass it explicitly in payload). |
| cross_domain | CrossDomainConfig | Controls cookie domain, bridge URL, and allowlist for multi-domain attribution. |
| site_id | string | Optional UUID for associating events with a registered site. Included in all event payloads as site_id. |
| site_host | string | Optional site host for audit/debug purposes. Included in all event payloads as site_host. If not provided, browser SDK uses window.location.host. |
| capture_query_params | string[] | Additional query keys to capture for attribution (merged into the default allowlist). |
| capture_all_query_params | boolean | Capture all query params after denylist/limits. Defaults to false. |
| query_param_denylist | string[] | Query keys that are never captured (exact-key matching; default includes sensitive keys like email, token, password). |
| max_captured_query_params | number | Maximum number of captured query keys stored per visitor (default 30). |
| max_query_param_value_length | number | Maximum stored length per captured query value (default 256). |
Server runtimes can also pass storage, storageDefaults, or transport via createNodeMark to fully control persistence and delivery.
Consent & Privacy
These behaviors matter for compliance-sensitive setups (GDPR-style consent, CMPs, enterprise security reviews):
- Consent gating: Use
require_consent: trueor'auto'so events andidentifyonly run after a positive consent signal. In'auto'mode, missing consent is treated as denied untilsetConsent('granted'). - TCF v2: Set
consent_source: { type: 'tcf', purposes: [/* IAB purpose IDs */] }so tracking follows your CMP’s current purpose consents (the SDK subscribes to CMP updates rather than relying on a one-time read). - Withdrawal:
setConsent('denied')stops tracking and clears stored attribution plus cookie-backed visitor identity where applicable. - Stricter identity hygiene: Enable
rotate_visitor_on_consent_changeif you want a freshvisitor_idafter a denied → granted transition. - DNT / GPC:
honor_dnt: trueblocks tracking when the browser reports Do Not Track or Global Privacy Control. - Data minimization: Use
before_sendto strip or redact fields before they leave the client; useon_errorfor observability without logging raw payloads. - Payloads cannot bypass consent via event properties; reserved fields are sanitized.
- Pre-consent attribution: URL attribution is held in memory only until consent is granted, then persisted.
- Cross-domain: First-party iframe bridges keep identifiers under your control.
- Delivery without long-lived local queues: Failed sends can be retried from a browser outbox with a 48-hour TTL; on tab hide / unload, pending items are flushed with
sendBeaconwhere possible to improve delivery without weakening consent checks. - IP / geo: IP is not read in the browser; it is taken server-side, hashed, and used for coarse geo only when allowed by consent and tenant settings.
For product-level privacy commitments and processor terms, rely on your OneLence agreement and documentation; this README describes SDK behavior only.
Visitor ID for server attribution
When you don't have an authenticated user ID, you can pass the SDK's visitor ID to your backend so server-side events (e.g. checkout, webhooks) are attributed to the same visitor as browser events.
Browser: Call Mark.getVisitorId() after init. If you use require_consent: true or 'auto', it returns undefined until consent is granted. Once available, send it in a header or request body to your API and use it as the visitor_id when calling the Node SDK or your ingestion.
Node: Call mark.getVisitorId() to read the visitor ID from the storage you passed to createNodeMark (e.g. from storageDefaults.visitor_id). Use it to associate server events with the same visitor.
The visitor ID is a pseudonymous, SDK-scoped identifier. Do not use it as a cross-site or long-term user identity; use it only for joining browser and server events within your attribution flow.
User Identification
The identify() method links anonymous visitors to known users. Recommended traits:
email(string) - User's email address. Will be hashed server-side for privacy.display_name(string) - User's display name or full name.language(string) - User's preferred language code (e.g.,'en','en-US','fr').phone(string) - User's phone number. Will be hashed server-side for privacy.
You can also include any custom traits as key-value pairs. All traits are stored in the user profile and can be used for segmentation and personalization.
Mark.identify('user_123', {
email: '[email protected]',
display_name: 'John Doe',
language: 'en-US',
plan: 'premium',
signup_date: '2024-01-15',
});Page Views
- Event name:
page_view(canonical). Use display names in your product UI if you prefer human-readable labels. - Autocapture (optional): set
autocapture: { pageview: true }(and optionallytrack_route_changes: true) to emit on first load and SPA route changes. If consent is required and not yet granted, initial pageview is deferred and emitted once consent is granted. - All SDK config and event fields use snake_case (
site_id,site_host, etc.) and map directly to stored payloads and database columns.
Extended autocapture (browser)
All of the following are opt-in under autocapture. They respect the same consent, DNT/GPC, and sampling rules as track() (see sample_rate: conversions are exempt; these modes are not conversion events). They are registered when Mark.init() runs.
| Flag | Event name(s) | Behavior |
| --- | --- | --- |
| click | data-mark-event value, or click | Listens for clicks. Default: only elements matching [data-mark-event]; set click: { selector: '.my-tracked' } to use a custom selector. Sends element_id, element_classes, truncated text, and href when applicable. |
| form_submit | form_submit | Listens for submit; sends form_id, form_name, action. |
| outbound_link | outbound_link_click | On click, if the link target is another origin, sends href. Uses preferBeacon: true so the event is more likely to fire before navigation. |
| scroll_depth | scroll_depth | Fires at 25%, 50%, 75%, and 100% of vertical scroll depth (once each per page load). Payload includes percent. |
| web_vitals | web_vital | Lazy-loads the web-vitals dependency and reports LCP, CLS, INP, and TTFB with metric, value, optional rating, and metric_id. If the module fails to load, only debug: true logs a warning. |
Example:
Mark.init({
key: 'pk_xxxxx',
require_consent: 'auto',
autocapture: {
pageview: true,
click: true,
// or: click: { selector: '[data-analytics]' },
form_submit: true,
outbound_link: true,
scroll_depth: true,
web_vitals: true,
},
});Flagging internal traffic
Use the is_internal marker to keep QA sessions, staff testing, or automated smoke tests out of customer-facing reports without dropping them on the client. The SDK supports it in two complementary ways:
1. Per-event field. Any track / conversion call can carry is_internal: true:
Mark.track('checkout_started', { value: 1299, is_internal: true });2. Persistent visitor flag. Set it once and every subsequent event is stamped automatically:
Mark.setInternal(true); // stamps is_internal: true on all future events
Mark.setInternal(false); // stops stamping
Mark.getInternal(); // booleanThe persistent flag is stored in the SDK's browser storage so it survives reloads. It is cleared automatically by Mark.reset() and by Mark.setConsent('denied').
Design note. The SDK intentionally does not read a magic URL parameter or write to a well-known localStorage key on its own — that would let anyone opt themselves out of your analytics just by visiting a URL, and it would bake a specific policy into every integration. Instead, your app decides when to flip the flag. A common pattern is a URL query parameter for staff onboarding:
// After Mark.init(...)
const params = new URLSearchParams(window.location.search);
if (params.get('onelence_internal') === '1') Mark.setInternal(true);
if (params.get('onelence_internal') === '0') Mark.setInternal(false);Other reasonable triggers: an authenticated admin/staff role, an internal IP range detected server-side, a feature flag, or an explicit toggle in your own settings UI.
Server-side the same API is available on NodeMark:
const mark = createNodeMark({ key: process.env.MARK_SECRET_KEY! });
mark.setInternal(true);
mark.track('backoffice_action'); // is_internal: trueis_internal is a first-class field on the backend: events are still ingested (so you can audit and debug integrations), but excluded from customer-facing reports by default.
Support
Need help? Reach out through your account team or file a ticket via the OneLence dashboard. Please include the SDK version, runtime (browser or Node), and any reproduction steps so we can assist quickly.
