@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 +97 -9
- package/dist/browser.es.js +100 -34
- package/dist/browser.es.js.map +1 -1
- package/dist/browser.umd.js +1 -1
- package/dist/browser.umd.js.map +1 -1
- package/dist/node.cjs +1 -1
- package/dist/node.cjs.map +1 -1
- package/dist/node.es.js +65 -12
- package/dist/node.es.js.map +1 -1
- package/dist/types/browser/BrowserStorage.d.ts +2 -0
- package/dist/types/browser/Mark.d.ts +16 -0
- package/dist/types/core/MarkCore.d.ts +24 -0
- package/dist/types/core/adapters.d.ts +2 -0
- package/dist/types/node/StatelessStorage.d.ts +2 -0
- package/dist/types/node/index.d.ts +6 -0
- package/dist/types/types.d.ts +12 -0
- package/package.json +1 -1
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
|
|
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
|
-
|
|
167
|
-
|
|
168
|
-
- In `'auto'` mode, missing consent is treated as denied until
|
|
169
|
-
-
|
|
170
|
-
-
|
|
171
|
-
-
|
|
172
|
-
-
|
|
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
|
|
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
|
|
package/dist/browser.es.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const
|
|
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
|
|
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,
|
|
26
|
-
class
|
|
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 ??
|
|
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 (
|
|
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
|
|
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 ??
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 ??
|
|
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
|
|
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
|
|
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:
|
|
477
|
-
retryAfterMs:
|
|
512
|
+
body: p,
|
|
513
|
+
retryAfterMs: w
|
|
478
514
|
}), new h(
|
|
479
|
-
`[Mark] Request rejected with status ${u.status}: ${
|
|
480
|
-
{ status: u.status, retryAfterMs:
|
|
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
|
|
489
|
-
throw new h(`[Mark] Network error: ${
|
|
524
|
+
const p = u instanceof Error ? u.message : String(u);
|
|
525
|
+
throw new h(`[Mark] Network error: ${p}`);
|
|
490
526
|
} finally {
|
|
491
|
-
clearTimeout(
|
|
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",
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
760
|
-
if (i.has(d) || i.has(
|
|
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 (!(
|
|
806
|
+
if (!(g in e) && Object.keys(e).length >= s)
|
|
765
807
|
break;
|
|
766
|
-
e[
|
|
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
|
}
|