@crelora/mark 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,6 +2,13 @@
2
2
 
3
3
  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.
4
4
 
5
+ Use it to feed [**OneLence**](https://onelence.com) with first‑party behavioral and conversion data so you can build **analytics**, **insights**, **signals**, and **decisions** on top of a unified event stream.
6
+
7
+ ### API keys and documentation
8
+
9
+ - **Keys:** Publishable keys (`pk_…`) for browser and secret keys (`sk_…`) for server-side use are available in the OneLence dashboard: [API keys](https://onelence.com/dash/settings/api-keys).
10
+ - **Guides:** For integration patterns, alternative integration types (e.g. server-only, tag managers), and deeper technical documentation, see the [Integrations overview](https://onelence.com/docs/integrations/overview).
11
+
5
12
  ## Installation
6
13
 
7
14
  ```bash
@@ -97,6 +104,7 @@ The Node factory accepts optional custom storage or transport adapters so you ca
97
104
  - `flush()` – flushes queued/persisted delivery items.
98
105
  - `reset()` – clears user/session/attribution state and rotates visitor identity for logout flows.
99
106
  - `getStats()` – returns runtime delivery stats `{ queued, sent, failed, dropped }`.
107
+ - `setInternal(value)` / `getInternal()` – flags the current visitor as internal traffic (QA, staff, smoke tests). While set, every outgoing event is stamped with `is_internal: true` so the backend can exclude it from customer-facing reports by default. See [Flagging internal traffic](#flagging-internal-traffic).
100
108
 
101
109
  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.
102
110
 
@@ -136,7 +144,7 @@ All config options use snake_case. Stored event payloads and database columns ma
136
144
 
137
145
  | Option | Type | Description |
138
146
  | --- | --- | --- |
139
- | `key` | `string` | Publishable (browser) or secret (server) key issued in the Crelora dashboard. |
147
+ | `key` | `string` | Publishable (browser) or secret (server) key from [OneLence API keys](https://onelence.com/dash/settings/api-keys). |
140
148
  | `debug` | `boolean` | Enables verbose console logging to help with integration tests. |
141
149
  | `before_send` | `(event) => event \| null` | Mutate/redact payloads before send, or return `null` to drop events. |
142
150
  | `on_error` | `(error, event?) => void` | Hook for transport and queue failures. |
@@ -147,6 +155,7 @@ All config options use snake_case. Stored event payloads and database columns ma
147
155
  | `rotate_visitor_on_consent_change` | `boolean` | Rotate `visitor_id` after denied -> granted transition. |
148
156
  | `batching` | `{ enabled?: boolean, max_size?: number, flush_interval_ms?: number, endpoint_path?: string }` | Optional batch mode (`/events` by default). |
149
157
  | `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). |
158
+ | `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. |
150
159
  | `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. |
151
160
  | `track_route_changes` | `boolean` | When `autocapture.pageview` is true, also emits on SPA route changes (pushState/replaceState/popstate); defaults to `true`. |
152
161
  | `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). |
@@ -163,13 +172,21 @@ Server runtimes can also pass `storage`, `storageDefaults`, or `transport` via `
163
172
 
164
173
  ## Consent & Privacy
165
174
 
166
- - Call `setConsent('denied')` to immediately halt tracking when visitors opt out and clear stored attribution/cookie identity state.
167
- - Use `require_consent: true` (or `'auto'`) to make the SDK wait until a positive consent signal is received.
168
- - In `'auto'` mode, missing consent is treated as denied until consent is explicitly granted.
169
- - Event payloads cannot bypass consent checks.
170
- - Attribution parameters are not persisted pre-consent; they are held in runtime memory until consent is granted.
171
- - Cross-domain mode supports first-party iframe bridges so identifiers remain in your control.
172
- - IP/Geo: IP is never taken from the browser; it is captured server-side, hashed, and used to derive coarse geo (country/region/city, coarse lat/lon) when allowed by consent/tenant settings.
175
+ These behaviors matter for **compliance-sensitive** setups (GDPR-style consent, CMPs, enterprise security reviews):
176
+
177
+ - **Consent gating:** Use `require_consent: true` or `'auto'` so events and `identify` only run after a positive consent signal. In `'auto'` mode, missing consent is treated as denied until `setConsent('granted')`.
178
+ - **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).
179
+ - **Withdrawal:** `setConsent('denied')` stops tracking and clears stored attribution plus cookie-backed visitor identity where applicable.
180
+ - **Stricter identity hygiene:** Enable `rotate_visitor_on_consent_change` if you want a fresh `visitor_id` after a denied → granted transition.
181
+ - **DNT / GPC:** `honor_dnt: true` blocks tracking when the browser reports Do Not Track or Global Privacy Control.
182
+ - **Data minimization:** Use `before_send` to strip or redact fields before they leave the client; use `on_error` for observability without logging raw payloads.
183
+ - **Payloads cannot bypass consent** via event properties; reserved fields are sanitized.
184
+ - **Pre-consent attribution:** URL attribution is held in memory only until consent is granted, then persisted.
185
+ - **Cross-domain:** First-party iframe bridges keep identifiers under your control.
186
+ - **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 **`sendBeacon`** where possible to improve delivery without weakening consent checks.
187
+ - **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.
188
+
189
+ For product-level privacy commitments and processor terms, rely on your OneLence agreement and [documentation](https://onelence.com/docs/integrations/overview); this README describes SDK behavior only.
173
190
 
174
191
  ## Visitor ID for server attribution
175
192
 
@@ -207,7 +224,78 @@ Mark.identify('user_123', {
207
224
  - Autocapture (optional): set `autocapture: { pageview: true }` (and optionally `track_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.
208
225
  - All SDK config and event fields use snake_case (`site_id`, `site_host`, etc.) and map directly to stored payloads and database columns.
209
226
 
227
+ ## Extended autocapture (browser)
228
+
229
+ 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.
230
+
231
+ | Flag | Event name(s) | Behavior |
232
+ | --- | --- | --- |
233
+ | `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. |
234
+ | `form_submit` | `form_submit` | Listens for `submit`; sends `form_id`, `form_name`, `action`. |
235
+ | `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. |
236
+ | `scroll_depth` | `scroll_depth` | Fires at **25%, 50%, 75%, and 100%** of vertical scroll depth (once each per page load). Payload includes `percent`. |
237
+ | `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. |
238
+
239
+ Example:
240
+
241
+ ```ts
242
+ Mark.init({
243
+ key: 'pk_xxxxx',
244
+ require_consent: 'auto',
245
+ autocapture: {
246
+ pageview: true,
247
+ click: true,
248
+ // or: click: { selector: '[data-analytics]' },
249
+ form_submit: true,
250
+ outbound_link: true,
251
+ scroll_depth: true,
252
+ web_vitals: true,
253
+ },
254
+ });
255
+ ```
256
+
257
+ ## Flagging internal traffic
258
+
259
+ 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:
260
+
261
+ **1. Per-event field.** Any `track` / `conversion` call can carry `is_internal: true`:
262
+
263
+ ```ts
264
+ Mark.track('checkout_started', { value: 1299, is_internal: true });
265
+ ```
266
+
267
+ **2. Persistent visitor flag.** Set it once and every subsequent event is stamped automatically:
268
+
269
+ ```ts
270
+ Mark.setInternal(true); // stamps is_internal: true on all future events
271
+ Mark.setInternal(false); // stops stamping
272
+ Mark.getInternal(); // boolean
273
+ ```
274
+
275
+ The 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')`.
276
+
277
+ **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:
278
+
279
+ ```ts
280
+ // After Mark.init(...)
281
+ const params = new URLSearchParams(window.location.search);
282
+ if (params.get('onelence_internal') === '1') Mark.setInternal(true);
283
+ if (params.get('onelence_internal') === '0') Mark.setInternal(false);
284
+ ```
285
+
286
+ 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.
287
+
288
+ **Server-side** the same API is available on `NodeMark`:
289
+
290
+ ```ts
291
+ const mark = createNodeMark({ key: process.env.MARK_SECRET_KEY! });
292
+ mark.setInternal(true);
293
+ mark.track('backoffice_action'); // is_internal: true
294
+ ```
295
+
296
+ `is_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.
297
+
210
298
  ## Support
211
299
 
212
- Need help? Reach out through your Crelora account team or file a ticket via the dashboard. Please include the SDK version, runtime (browser or Node), and any reproduction steps so we can assist quickly.
300
+ 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.
213
301
 
@@ -1,4 +1,4 @@
1
- const S = "https://ingest.onelence.com";
1
+ const I = "https://ingest.onelence.com";
2
2
  class h extends Error {
3
3
  status;
4
4
  retryAfterMs;
@@ -6,7 +6,7 @@ class h extends Error {
6
6
  super(t), this.name = "TransportError", this.status = e.status, this.retryAfterMs = e.retryAfterMs;
7
7
  }
8
8
  }
9
- function I(c) {
9
+ function S(c) {
10
10
  return !(typeof c != "number" || c < 400 || c >= 500 || c === 408 || c === 429);
11
11
  }
12
12
  function E(c) {
@@ -22,10 +22,10 @@ function E(c) {
22
22
  return s > 0 ? s : 0;
23
23
  }
24
24
  }
25
- const x = 5, q = 300, T = 15e3, k = 2880 * 60 * 1e3;
26
- class D {
25
+ const x = 5, q = 300, T = 15e3, D = 2880 * 60 * 1e3;
26
+ class C {
27
27
  constructor(t, e = {}) {
28
- this.transport = t, this.maxAttempts = e.maxAttempts ?? x, this.baseBackoffMs = e.baseBackoffMs ?? q, this.maxBackoffMs = e.maxBackoffMs ?? T, this.maxItemAgeMs = e.maxItemAgeMs ?? k, this.debug = e.debug ?? !1, this.loadPersisted = e.loadPersisted, this.savePersisted = e.savePersisted, this.onError = e.onError;
28
+ this.transport = t, this.maxAttempts = e.maxAttempts ?? x, this.baseBackoffMs = e.baseBackoffMs ?? q, this.maxBackoffMs = e.maxBackoffMs ?? T, this.maxItemAgeMs = e.maxItemAgeMs ?? D, this.debug = e.debug ?? !1, this.loadPersisted = e.loadPersisted, this.savePersisted = e.savePersisted, this.onError = e.onError;
29
29
  const i = this.loadPersisted?.() ?? [];
30
30
  if (i.length > 0) {
31
31
  const s = Date.now();
@@ -118,7 +118,7 @@ class D {
118
118
  } catch (i) {
119
119
  this.failed += 1, this.onError?.(i, e.data);
120
120
  const s = i instanceof h ? i.status : void 0;
121
- if (I(s)) {
121
+ if (S(s)) {
122
122
  this.queue.shift(), this.dropped += 1, this.persist(), this.debug && console.error("[Mark] Dropping event after non-retriable status", s, e.path);
123
123
  continue;
124
124
  }
@@ -147,7 +147,7 @@ class D {
147
147
  }
148
148
  }
149
149
  }
150
- const C = /* @__PURE__ */ new Set(["event_name", "user_id", "consent_state", "source", "is_conversion"]), U = /* @__PURE__ */ new Set([
150
+ const U = /* @__PURE__ */ new Set(["event_name", "user_id", "consent_state", "source", "is_conversion"]), k = /* @__PURE__ */ new Set([
151
151
  "user_id",
152
152
  "visitor_id",
153
153
  "click_id",
@@ -159,10 +159,10 @@ const C = /* @__PURE__ */ new Set(["event_name", "user_id", "consent_state", "so
159
159
  class L {
160
160
  constructor(t, e) {
161
161
  this.deps = e, this.validateConfig(t), this.config = {
162
- endpoint: t.endpoint ?? S,
162
+ endpoint: t.endpoint ?? I,
163
163
  ...t,
164
164
  include_page_context: t.include_page_context ?? !0
165
- }, this.consentRequirement = t.require_consent ?? !1, this.siteId = t.site_id, this.siteHost = t.site_host, this.sessionTimeoutMs = t.session_timeout_ms ?? 1800 * 1e3, this.queue = new D(this.deps.transport, {
165
+ }, this.consentRequirement = t.require_consent ?? !1, this.siteId = t.site_id, this.siteHost = t.site_host, this.sessionTimeoutMs = t.session_timeout_ms ?? 1800 * 1e3, this.queue = new C(this.deps.transport, {
166
166
  debug: this.config.debug,
167
167
  loadPersisted: () => this.deps.storage.getOutbox?.() ?? [],
168
168
  savePersisted: (i) => this.deps.storage.setOutbox?.(i),
@@ -214,7 +214,7 @@ class L {
214
214
  };
215
215
  i && (a.is_conversion = !0);
216
216
  const l = r.site_id ?? this.siteId, f = r.site_host ?? this.siteHost;
217
- l && (a.site_id = l), f && (a.site_host = f), this.config.include_page_context && typeof window < "u" && (this.applyPageContext(a), !f && a.site && (a.site_host = a.site));
217
+ l && (a.site_id = l), f && (a.site_host = f), this.config.include_page_context && typeof window < "u" && (this.applyPageContext(a), !f && a.site && (a.site_host = a.site)), this.applyInternalFlag(a, r.is_internal);
218
218
  const d = this.config.before_send ? this.config.before_send(a) : a;
219
219
  return d ? (this.ensureSession(), this.config.batching?.enabled && !i && !s?.preferBeacon ? (this.enqueueBatch(d), !0) : (this.queue.enqueue("/event", { ...d, __prefer_beacon: s?.preferBeacon === !0 }), !0)) : !0;
220
220
  }
@@ -234,7 +234,7 @@ class L {
234
234
  ...this.sanitizeIdentifyTraits(e),
235
235
  ...this.getIdentityFields()
236
236
  };
237
- this.siteId && (i.site_id = this.siteId), this.siteHost && (i.site_host = this.siteHost);
237
+ this.siteId && (i.site_id = this.siteId), this.siteHost && (i.site_host = this.siteHost), this.applyInternalFlag(i);
238
238
  const s = this.config.before_send ? this.config.before_send(i) : i;
239
239
  s && this.queue.enqueue("/identify", s);
240
240
  }
@@ -253,14 +253,14 @@ class L {
253
253
  }
254
254
  setConsent(t) {
255
255
  const e = this.deps.storage.getConsentStatus();
256
- this.deps.storage.setConsentStatus(t), t === "denied" ? (this.deps.storage.clearAttribution?.(), this.deps.storage.clearCookieVisitorId?.()) : t === "granted" && e === "denied" && this.config.rotate_visitor_on_consent_change && this.deps.storage.rotateVisitorId?.();
256
+ this.deps.storage.setConsentStatus(t), t === "denied" ? (this.deps.storage.clearAttribution?.(), this.deps.storage.clearCookieVisitorId?.(), this.deps.storage.setInternal?.(!1)) : t === "granted" && e === "denied" && this.config.rotate_visitor_on_consent_change && this.deps.storage.rotateVisitorId?.();
257
257
  const i = {
258
258
  visitor_id: this.deps.storage.getVisitorId(),
259
259
  consent_state: t,
260
260
  source: "sdk",
261
261
  message_id: this.createMessageId()
262
262
  };
263
- this.siteId && (i.site_id = this.siteId), this.siteHost && (i.site_host = this.siteHost);
263
+ this.siteId && (i.site_id = this.siteId), this.siteHost && (i.site_host = this.siteHost), this.applyInternalFlag(i);
264
264
  const s = this.config.before_send ? this.config.before_send(i) : i;
265
265
  s && this.queue.enqueue("/consent", s);
266
266
  }
@@ -272,15 +272,51 @@ class L {
272
272
  query_params: void 0,
273
273
  session_id: void 0,
274
274
  session_started_at: void 0,
275
- last_activity_at: void 0
275
+ last_activity_at: void 0,
276
+ is_internal: void 0
276
277
  }), this.deps.storage.rotateVisitorId?.();
277
278
  }
279
+ /**
280
+ * Marks or unmarks the current visitor as internal traffic. While set, every
281
+ * subsequent event (track/identify/conversion/consent) is stamped with
282
+ * `is_internal: true`, so the backend can exclude it from customer-facing
283
+ * reports by default.
284
+ *
285
+ * The flag is persisted via the storage adapter (browser: localStorage) so
286
+ * it survives reloads, and is cleared by `reset()` and by
287
+ * `setConsent('denied')`.
288
+ */
289
+ setInternal(t) {
290
+ this.deps.storage.setInternal?.(!!t);
291
+ }
292
+ /**
293
+ * Returns the currently persisted internal-traffic flag, if any.
294
+ */
295
+ getInternal() {
296
+ return !!this.deps.storage.getInternal?.();
297
+ }
278
298
  flush() {
279
299
  return this.flushBatch(), this.queue.flush();
280
300
  }
281
301
  getStats() {
282
302
  return this.queue.getStats();
283
303
  }
304
+ /**
305
+ * Stamps `is_internal: true` on the payload when either:
306
+ * - the persistent visitor flag is set (via setInternal), or
307
+ * - the caller passed `is_internal: true` on this specific event.
308
+ *
309
+ * Explicit `is_internal: false` on a single event wins over the visitor flag
310
+ * so individual calls can opt out.
311
+ */
312
+ applyInternalFlag(t, e) {
313
+ if (e === !1) {
314
+ delete t.is_internal;
315
+ return;
316
+ }
317
+ const i = this.deps.storage.getInternal?.() === !0;
318
+ e === !0 || i ? t.is_internal = !0 : delete t.is_internal;
319
+ }
284
320
  getIdentityFields(t) {
285
321
  const e = t?.visitor_id ?? this.deps.storage.getVisitorId(), i = t?.user_id ?? this.deps.storage.getUserId?.(), s = t?.click_id ?? this.deps.storage.getLastClickId(), r = t?.campaign_id ?? this.deps.storage.getCampaignId(), o = t?.session_id ?? this.deps.storage.getSessionId?.(), a = this.deps.storage.getQueryParams() ?? {}, l = t?.query ?? {}, f = { ...a, ...l }, d = {};
286
322
  return e && (d.visitor_id = e), i && (d.user_id = i), s && (d.click_id = s), r && (d.campaign_id = r), o && (d.session_id = o), Object.keys(f).length > 0 && (d.query = f), d;
@@ -296,13 +332,13 @@ class L {
296
332
  sanitizeTrackData(t) {
297
333
  const e = {};
298
334
  for (const [i, s] of Object.entries(t))
299
- C.has(i) || (e[i] = s);
335
+ U.has(i) || (e[i] = s);
300
336
  return e;
301
337
  }
302
338
  sanitizeIdentifyTraits(t) {
303
339
  const e = {};
304
340
  for (const [i, s] of Object.entries(t))
305
- U.has(i) || (e[i] = s);
341
+ k.has(i) || (e[i] = s);
306
342
  return e;
307
343
  }
308
344
  validateConfig(t) {
@@ -426,7 +462,7 @@ class P {
426
462
  endpoint;
427
463
  pending = /* @__PURE__ */ new Set();
428
464
  constructor(t) {
429
- this.config = t, this.endpoint = t.endpoint ?? S;
465
+ this.config = t, this.endpoint = t.endpoint ?? I;
430
466
  }
431
467
  async send(t, e, i) {
432
468
  const s = this.sendInternal(t, e, i);
@@ -456,7 +492,7 @@ class P {
456
492
  throw this.config.debug && console.error("[Mark] Global fetch is not available in this runtime."), new h("[Mark] Global fetch is not available in this runtime.");
457
493
  const l = this.config.request_timeout_ms ?? 1e4, f = new AbortController();
458
494
  let d = !1;
459
- const p = setTimeout(() => {
495
+ const g = setTimeout(() => {
460
496
  d = !0, f.abort();
461
497
  }, l);
462
498
  try {
@@ -468,16 +504,16 @@ class P {
468
504
  signal: f.signal
469
505
  });
470
506
  if (!u.ok) {
471
- const g = await this.readErrorSnippet(u), y = E(u.headers.get("Retry-After"));
507
+ const p = await this.readErrorSnippet(u), w = E(u.headers.get("Retry-After"));
472
508
  throw this.config.debug && console.error("[Mark] Request rejected", {
473
509
  url: s,
474
510
  status: u.status,
475
511
  statusText: u.statusText,
476
- body: g,
477
- retryAfterMs: y
512
+ body: p,
513
+ retryAfterMs: w
478
514
  }), new h(
479
- `[Mark] Request rejected with status ${u.status}: ${g}`,
480
- { status: u.status, retryAfterMs: y }
515
+ `[Mark] Request rejected with status ${u.status}: ${p}`,
516
+ { status: u.status, retryAfterMs: w }
481
517
  );
482
518
  }
483
519
  } catch (u) {
@@ -485,10 +521,10 @@ class P {
485
521
  throw u;
486
522
  if (d)
487
523
  throw new h(`[Mark] Request timed out after ${l}ms`, { status: 408 });
488
- const g = u instanceof Error ? u.message : String(u);
489
- throw new h(`[Mark] Network error: ${g}`);
524
+ const p = u instanceof Error ? u.message : String(u);
525
+ throw new h(`[Mark] Network error: ${p}`);
490
526
  } finally {
491
- clearTimeout(p);
527
+ clearTimeout(g);
492
528
  }
493
529
  }
494
530
  joinUrl(t, e) {
@@ -503,7 +539,7 @@ class P {
503
539
  }
504
540
  }
505
541
  }
506
- const R = "crelora_mark_data", B = "crelora_mark_outbox", w = "crelora_mark_vid";
542
+ const R = "crelora_mark_data", B = "crelora_mark_outbox", y = "crelora_mark_vid";
507
543
  class F {
508
544
  data;
509
545
  storageKey;
@@ -546,6 +582,12 @@ class F {
546
582
  getLastActivityAt() {
547
583
  return this.data.last_activity_at;
548
584
  }
585
+ getInternal() {
586
+ return this.data.is_internal;
587
+ }
588
+ setInternal(t) {
589
+ t ? this.update({ is_internal: !0 }) : this.update({ is_internal: void 0 });
590
+ }
549
591
  update(t) {
550
592
  this.data = { ...this.data, ...t }, this.save();
551
593
  }
@@ -560,7 +602,7 @@ class F {
560
602
  });
561
603
  }
562
604
  clearCookieVisitorId() {
563
- this.setCookie(w, "", -1);
605
+ this.setCookie(y, "", -1);
564
606
  }
565
607
  rotateVisitorId() {
566
608
  this.update({ visitor_id: this.generateUUID() });
@@ -592,7 +634,7 @@ class F {
592
634
  return JSON.parse(e);
593
635
  } catch {
594
636
  }
595
- const t = this.getCookie(w);
637
+ const t = this.getCookie(y);
596
638
  return t ? { visitor_id: t } : {};
597
639
  }
598
640
  save() {
@@ -601,7 +643,7 @@ class F {
601
643
  localStorage.setItem(this.storageKey, JSON.stringify(this.data));
602
644
  } catch {
603
645
  }
604
- this.data.visitor_id && this.isCookieEnabled() && (this.setCookie(w, this.data.visitor_id, 365), this.options.bridge?.url && this.bridgeReady && this.postBridgeMessage({
646
+ this.data.visitor_id && this.isCookieEnabled() && (this.setCookie(y, this.data.visitor_id, 365), this.options.bridge?.url && this.bridgeReady && this.postBridgeMessage({
605
647
  type: "MARK_SYNC_UPDATE",
606
648
  visitorId: this.data.visitor_id
607
649
  }));
@@ -756,14 +798,14 @@ function j(c, t) {
756
798
  const d = m(l);
757
799
  if (!d)
758
800
  continue;
759
- const p = K[d] ?? d;
760
- if (i.has(d) || i.has(p) || a && !a.has(d))
801
+ const g = K[d] ?? d;
802
+ if (i.has(d) || i.has(g) || a && !a.has(d))
761
803
  continue;
762
804
  const u = f.trim();
763
805
  if (u) {
764
- if (!(p in e) && Object.keys(e).length >= s)
806
+ if (!(g in e) && Object.keys(e).length >= s)
765
807
  break;
766
- e[p] = u.slice(0, r);
808
+ e[g] = u.slice(0, r);
767
809
  }
768
810
  }
769
811
  return e;
@@ -877,6 +919,30 @@ class n {
877
919
  static flush() {
878
920
  return n.client ? n.client.flush() : Promise.resolve();
879
921
  }
922
+ /**
923
+ * Flags (or un-flags) the current visitor as internal traffic. Persists
924
+ * across reloads via the SDK's browser storage. Cleared by `reset()` and by
925
+ * `setConsent('denied')`.
926
+ *
927
+ * The policy for deciding *when* to call this (URL query parameter, auth
928
+ * role, IP, feature flag, etc.) is intentionally left to the host app.
929
+ *
930
+ * @example
931
+ * // URL-based opt-in: call once during app bootstrap, after Mark.init()
932
+ * const params = new URLSearchParams(window.location.search);
933
+ * if (params.get('onelence_internal') === '1') Mark.setInternal(true);
934
+ * if (params.get('onelence_internal') === '0') Mark.setInternal(false);
935
+ */
936
+ static setInternal(t) {
937
+ if (!n.client) {
938
+ n.config?.debug && console.warn("[Mark] Not initialized. Call init() first.");
939
+ return;
940
+ }
941
+ n.client.setInternal(t);
942
+ }
943
+ static getInternal() {
944
+ return n.client?.getInternal() ?? !1;
945
+ }
880
946
  static reset() {
881
947
  n.client?.reset(), n.pendingAttribution = {}, n.lastPageviewHref = null;
882
948
  }