@drivemetadata-ai/sdk 0.1.1-beta.2 → 0.1.1-beta.4

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.
@@ -3,12 +3,62 @@
3
3
  // src/react/DmdProvider.tsx
4
4
  import React from "react";
5
5
 
6
+ // src/browser/core/browser-config.ts
7
+ function setIfDefined(target, key, value) {
8
+ if (value !== void 0) {
9
+ target[key] = value;
10
+ }
11
+ }
12
+ function normalizeBrowserConfig(input) {
13
+ const normalized = {
14
+ ...input,
15
+ clientId: input.clientId ?? "",
16
+ workspaceId: input.workspaceId ?? "",
17
+ appId: input.appId ?? ""
18
+ };
19
+ setIfDefined(normalized, "apiHost", input.apiHost);
20
+ setIfDefined(normalized, "capturePageview", input.capturePageview);
21
+ setIfDefined(normalized, "capturePageleave", input.capturePageleave);
22
+ setIfDefined(normalized, "captureDeadClicks", input.captureDeadClicks);
23
+ setIfDefined(normalized, "crossSubdomainCookie", input.crossSubdomainCookie);
24
+ setIfDefined(normalized, "sessionIdleTimeoutSeconds", input.sessionIdleTimeoutSeconds);
25
+ setIfDefined(normalized, "schemaValidation", input.schemaValidation);
26
+ setIfDefined(normalized, "beforeSend", input.beforeSend);
27
+ setIfDefined(normalized, "persistence", input.disablePersistence === true ? "none" : input.persistence);
28
+ return normalized;
29
+ }
30
+
31
+ // src/core/uuid.ts
32
+ function createUuid() {
33
+ const cryptoApi = globalThis.crypto;
34
+ if (typeof cryptoApi?.randomUUID === "function") {
35
+ return cryptoApi.randomUUID();
36
+ }
37
+ const bytes = new Uint8Array(16);
38
+ if (typeof cryptoApi?.getRandomValues === "function") {
39
+ cryptoApi.getRandomValues(bytes);
40
+ } else {
41
+ for (let index = 0; index < bytes.length; index += 1) {
42
+ bytes[index] = Math.floor(Math.random() * 256);
43
+ }
44
+ }
45
+ bytes[6] = (bytes[6] ?? 0) & 15 | 64;
46
+ bytes[8] = (bytes[8] ?? 0) & 63 | 128;
47
+ const hex = Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
48
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
49
+ }
50
+ function isUuid(value) {
51
+ return typeof value === "string" && /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value);
52
+ }
53
+ function ensureUuid(value) {
54
+ return isUuid(value) ? value : createUuid();
55
+ }
56
+
6
57
  // src/core/backend-payload.ts
7
58
  function formatUtcTimestamp(value) {
8
- if (typeof value !== "string") return (/* @__PURE__ */ new Date()).toISOString().replace("T", " ").slice(0, 19);
9
- const date = new Date(value);
10
- if (Number.isNaN(date.getTime())) return value;
11
- return date.toISOString().replace("T", " ").slice(0, 19);
59
+ const date = typeof value === "string" || typeof value === "number" || value instanceof Date ? new Date(value) : /* @__PURE__ */ new Date();
60
+ const safeDate = Number.isNaN(date.getTime()) ? /* @__PURE__ */ new Date() : date;
61
+ return safeDate.toISOString().replace("T", " ").slice(0, 19);
12
62
  }
13
63
  function cleanObject(value) {
14
64
  if (Array.isArray(value)) {
@@ -39,15 +89,19 @@ function createBackendCollectorPayload(input) {
39
89
  };
40
90
  const metaData = {
41
91
  ...normalizedEventData,
42
- requestId: input.requestId,
92
+ requestId: ensureUuid(typeof input.requestId === "string" ? input.requestId : void 0),
43
93
  timestamp,
44
94
  eventType: input.eventType,
45
95
  requestFrom: input.requestFrom ?? "3",
46
96
  clientId: input.clientId,
47
97
  workspaceId: input.workspaceId,
48
98
  token: input.token,
49
- anonymousId: eventData.anonymousId ?? input.anonymousId,
50
- sessionId: eventData.sessionId ?? input.sessionId,
99
+ anonymousId: ensureUuid(
100
+ typeof eventData.anonymousId === "string" ? eventData.anonymousId : typeof input.anonymousId === "string" ? input.anonymousId : void 0
101
+ ),
102
+ sessionId: ensureUuid(
103
+ typeof eventData.sessionId === "string" ? eventData.sessionId : typeof input.sessionId === "string" ? input.sessionId : void 0
104
+ ),
51
105
  ua: input.ua,
52
106
  appDetails: { app_id: input.appId },
53
107
  page: { ...page2, url: page2.url ?? input.pageUrl },
@@ -58,12 +112,45 @@ function createBackendCollectorPayload(input) {
58
112
  return cleanObject(payload);
59
113
  }
60
114
 
61
- // src/core/environment.ts
62
- function getBrowserWindow() {
63
- return typeof window === "undefined" ? void 0 : window;
115
+ // src/core/backend-schema.ts
116
+ var requiredMetaDataFields = [
117
+ "requestId",
118
+ "timestamp",
119
+ "eventType",
120
+ "requestFrom",
121
+ "clientId",
122
+ "workspaceId",
123
+ "anonymousId",
124
+ "sessionId"
125
+ ];
126
+ function isPresent(value) {
127
+ return value !== null && value !== void 0 && value !== "";
128
+ }
129
+ function validateBackendCollectorPayload(payload) {
130
+ const errors = [];
131
+ const metaData = payload.metaData;
132
+ if (!metaData || typeof metaData !== "object" || Array.isArray(metaData)) {
133
+ return {
134
+ ok: false,
135
+ errors: ["metaData is required"]
136
+ };
137
+ }
138
+ const metadataRecord = metaData;
139
+ for (const field of requiredMetaDataFields) {
140
+ if (!isPresent(metadataRecord[field])) {
141
+ errors.push(`metaData.${field} is required`);
142
+ }
143
+ }
144
+ if (isPresent(metadataRecord.eventType) && typeof metadataRecord.eventType !== "string") {
145
+ errors.push("metaData.eventType must be a string");
146
+ }
147
+ return {
148
+ ok: errors.length === 0,
149
+ errors
150
+ };
64
151
  }
65
152
 
66
- // src/browser/core/consent.ts
153
+ // src/core/consent.ts
67
154
  var purposes = [
68
155
  "analytics",
69
156
  "advertising",
@@ -103,6 +190,335 @@ function canCollectPurpose(consent, purpose) {
103
190
  return consent[purpose] === "granted";
104
191
  }
105
192
 
193
+ // src/core/event-schema.ts
194
+ var reservedKeys = /* @__PURE__ */ new Set(["messageId", "timestamp", "type", "event", "anonymousId", "userId", "context"]);
195
+ function validateEventEnvelope(envelope, options = {}) {
196
+ if (options.mode === "off") return { ok: true, errors: [] };
197
+ const errors = [];
198
+ if (typeof envelope.type !== "string") errors.push("type must be a string");
199
+ if (envelope.type === "track" && typeof envelope.event !== "string") {
200
+ errors.push("track event must include event name");
201
+ }
202
+ if (typeof envelope.messageId !== "string") errors.push("messageId must be a string");
203
+ if (typeof envelope.timestamp !== "string") errors.push("timestamp must be an ISO string");
204
+ for (const key of Object.keys(envelope.properties ?? {})) {
205
+ if (reservedKeys.has(key)) errors.push(`properties.${key} is reserved`);
206
+ }
207
+ return { ok: errors.length === 0, errors };
208
+ }
209
+
210
+ // src/core/environment.ts
211
+ function getBrowserWindow() {
212
+ return typeof window === "undefined" ? void 0 : window;
213
+ }
214
+
215
+ // src/core/privacy.ts
216
+ var sensitiveKeys = /* @__PURE__ */ new Set([
217
+ "email",
218
+ "phone",
219
+ "mobile",
220
+ "address",
221
+ "address1",
222
+ "address2",
223
+ "first_name",
224
+ "last_name",
225
+ "name",
226
+ "token",
227
+ "secret",
228
+ "password",
229
+ "session",
230
+ "cookie"
231
+ ]);
232
+ function sanitizeValue(value, allow, path = []) {
233
+ if (Array.isArray(value)) {
234
+ return value.map((item) => sanitizeValue(item, allow, path));
235
+ }
236
+ if (value && typeof value === "object") {
237
+ return Object.fromEntries(
238
+ Object.entries(value).filter(([key]) => {
239
+ const lowerKey = key.toLowerCase();
240
+ const lowerPath = path.map((item) => item.toLowerCase());
241
+ const isEcommerceItemName = lowerKey === "name" && lowerPath.includes("ecommerce") && lowerPath.includes("items");
242
+ return isEcommerceItemName || !sensitiveKeys.has(lowerKey) || allow.has(lowerKey);
243
+ }).map(([key, nestedValue]) => [key, sanitizeValue(nestedValue, allow, [...path, key])])
244
+ );
245
+ }
246
+ return value;
247
+ }
248
+ function sanitizeProperties(input, allowRawKeys = []) {
249
+ const allow = new Set(allowRawKeys.map((key) => key.toLowerCase()));
250
+ return sanitizeValue(input, allow);
251
+ }
252
+
253
+ // src/core/attribution.ts
254
+ var dmdUtmKeys = [
255
+ "utm_source",
256
+ "utm_medium",
257
+ "utm_campaign",
258
+ "utm_term",
259
+ "utm_content",
260
+ "utm_id"
261
+ ];
262
+ var dmdCampaignKeys = ["campaign_id", "ad_id"];
263
+ var dmdClickIdSources = [
264
+ ["gclid", 2],
265
+ ["fbclid", 3],
266
+ ["ScCid", 1],
267
+ ["li_fat_id", 4]
268
+ ];
269
+ function cleanAttributionRecord(record) {
270
+ const cleaned = Object.fromEntries(
271
+ Object.entries(record).filter(([, value]) => value !== null && value !== void 0 && value !== "")
272
+ );
273
+ return Object.keys(cleaned).length > 0 ? cleaned : void 0;
274
+ }
275
+ function objectValue(value) {
276
+ return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
277
+ }
278
+ function mergeAttributionRecords(explicitProperties, stored) {
279
+ const attributionData = objectValue(explicitProperties.attributionData);
280
+ const utmParameter = objectValue(explicitProperties.utmParameter);
281
+ return {
282
+ ...explicitProperties,
283
+ ...stored.attributionData || attributionData ? { attributionData: { ...stored.attributionData ?? {}, ...attributionData ?? {} } } : {},
284
+ ...stored.utmParameter || utmParameter ? { utmParameter: { ...stored.utmParameter ?? {}, ...utmParameter ?? {} } } : {}
285
+ };
286
+ }
287
+
288
+ // src/browser/core/attribution.ts
289
+ function getCurrentUrl() {
290
+ return getBrowserWindow()?.location?.href;
291
+ }
292
+ function getCookie(name) {
293
+ const cookie = getBrowserWindow()?.document?.cookie;
294
+ if (!cookie) return void 0;
295
+ const escapedName = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
296
+ const match = new RegExp(`(?:^|; )${escapedName}=([^;]*)`).exec(cookie);
297
+ return match ? decodeURIComponent(match[1] ?? "") : void 0;
298
+ }
299
+ function getParams(url) {
300
+ return new URL(url, "https://placeholder.local").searchParams;
301
+ }
302
+ function setIfPresent(persistence, key, value) {
303
+ if (value !== null && value !== "") {
304
+ persistence.setItem(key, value);
305
+ }
306
+ }
307
+ function getStoredString(persistence, key) {
308
+ const value = persistence.getItem(key);
309
+ return value === null || value === "" ? void 0 : value;
310
+ }
311
+ function getStoredNumberOrString(persistence, key) {
312
+ const value = getStoredString(persistence, key);
313
+ if (value === void 0) return void 0;
314
+ const parsed = Number.parseInt(value, 10);
315
+ return Number.isNaN(parsed) ? value : parsed;
316
+ }
317
+ function captureAttributionFromUrl(persistence, url = getCurrentUrl()) {
318
+ if (!url) return;
319
+ const params = getParams(url);
320
+ for (const key of dmdUtmKeys) {
321
+ setIfPresent(persistence, key, params.get(key));
322
+ }
323
+ for (const key of dmdCampaignKeys) {
324
+ setIfPresent(persistence, key, params.get(key));
325
+ }
326
+ for (const [param, sdkPubId] of dmdClickIdSources) {
327
+ const value = params.get(param);
328
+ if (value) {
329
+ persistence.setItem("unique_id", value);
330
+ persistence.setItem("sdk_pub_id", String(sdkPubId));
331
+ }
332
+ }
333
+ }
334
+ function getStoredAttributionData(persistence) {
335
+ return cleanAttributionRecord({
336
+ unique_id: getStoredString(persistence, "unique_id"),
337
+ sdk_pub_id: getStoredNumberOrString(persistence, "sdk_pub_id"),
338
+ fbc: getStoredString(persistence, "fbc") ?? getCookie("_fbc"),
339
+ fbp: getStoredString(persistence, "fbp") ?? getCookie("_fbp"),
340
+ campaign_id: getStoredString(persistence, "campaign_id"),
341
+ ad_id: getStoredString(persistence, "ad_id")
342
+ });
343
+ }
344
+ function getStoredUtmParameter(persistence) {
345
+ return cleanAttributionRecord(Object.fromEntries(dmdUtmKeys.map((key) => [key, getStoredString(persistence, key)])));
346
+ }
347
+ function mergeStoredAttribution(properties, persistence) {
348
+ const stored = {};
349
+ const attributionData = getStoredAttributionData(persistence);
350
+ const utmParameter = getStoredUtmParameter(persistence);
351
+ if (attributionData !== void 0) stored.attributionData = attributionData;
352
+ if (utmParameter !== void 0) stored.utmParameter = utmParameter;
353
+ return mergeAttributionRecords(properties, stored);
354
+ }
355
+
356
+ // src/browser/core/autocapture.ts
357
+ function installAutocapture(config) {
358
+ const trackedPageUrls = /* @__PURE__ */ new Set();
359
+ const timers = /* @__PURE__ */ new Set();
360
+ const pageviewDelayMs = config.pageviewDelayMs ?? 500;
361
+ const history = config.browserWindow.history;
362
+ const originalPushState = history?.pushState;
363
+ const originalReplaceState = history?.replaceState;
364
+ function clearTimer(timer) {
365
+ timers.delete(timer);
366
+ clearTimeout(timer);
367
+ }
368
+ function canonicalUrl() {
369
+ return config.browserWindow.location.href;
370
+ }
371
+ function trackCurrentPageview() {
372
+ if (!config.capturePageview) return;
373
+ const url = canonicalUrl();
374
+ if (trackedPageUrls.has(url)) return;
375
+ trackedPageUrls.add(url);
376
+ config.onPageView();
377
+ }
378
+ function schedulePageview(delayMs) {
379
+ if (!config.capturePageview) return;
380
+ const timer = setTimeout(() => {
381
+ timers.delete(timer);
382
+ trackCurrentPageview();
383
+ }, delayMs);
384
+ timers.add(timer);
385
+ }
386
+ function handleRouteChange() {
387
+ config.onRouteChange?.();
388
+ trackCurrentPageview();
389
+ }
390
+ function wrapHistoryMethod(method) {
391
+ if (!history) return;
392
+ const original = history[method];
393
+ if (typeof original !== "function") return;
394
+ history[method] = function wrappedHistoryMethod(...args) {
395
+ const result = original.apply(this, args);
396
+ const timer = setTimeout(() => {
397
+ timers.delete(timer);
398
+ handleRouteChange();
399
+ }, 0);
400
+ timers.add(timer);
401
+ return result;
402
+ };
403
+ }
404
+ function handlePopOrHashChange() {
405
+ handleRouteChange();
406
+ }
407
+ function handlePageLeave() {
408
+ if (config.capturePageleave) {
409
+ config.onPageLeave();
410
+ }
411
+ }
412
+ schedulePageview(pageviewDelayMs);
413
+ wrapHistoryMethod("pushState");
414
+ wrapHistoryMethod("replaceState");
415
+ const canListen = typeof config.browserWindow.addEventListener === "function" && typeof config.browserWindow.removeEventListener === "function";
416
+ if (canListen) {
417
+ config.browserWindow.addEventListener("popstate", handlePopOrHashChange);
418
+ config.browserWindow.addEventListener("hashchange", handlePopOrHashChange);
419
+ config.browserWindow.addEventListener("beforeunload", handlePageLeave);
420
+ }
421
+ return {
422
+ cleanup() {
423
+ for (const timer of Array.from(timers)) {
424
+ clearTimer(timer);
425
+ }
426
+ if (history && originalPushState) history.pushState = originalPushState;
427
+ if (history && originalReplaceState) history.replaceState = originalReplaceState;
428
+ if (canListen) {
429
+ config.browserWindow.removeEventListener("popstate", handlePopOrHashChange);
430
+ config.browserWindow.removeEventListener("hashchange", handlePopOrHashChange);
431
+ config.browserWindow.removeEventListener("beforeunload", handlePageLeave);
432
+ }
433
+ }
434
+ };
435
+ }
436
+
437
+ // src/core/payload-size.ts
438
+ var preserveStringKeys = /* @__PURE__ */ new Set([
439
+ "requestId",
440
+ "eventType",
441
+ "requestFrom",
442
+ "clientId",
443
+ "workspaceId",
444
+ "anonymousId",
445
+ "sessionId",
446
+ "token"
447
+ ]);
448
+ function payloadByteLength(payload) {
449
+ const serialized = JSON.stringify(payload);
450
+ if (typeof TextEncoder !== "undefined") {
451
+ return new TextEncoder().encode(serialized).length;
452
+ }
453
+ return serialized.length;
454
+ }
455
+ function clonePayload(payload) {
456
+ try {
457
+ return JSON.parse(JSON.stringify(payload));
458
+ } catch {
459
+ return void 0;
460
+ }
461
+ }
462
+ function truncateValue(value, truncateStringLength, key) {
463
+ if (typeof value === "string") {
464
+ if (key && preserveStringKeys.has(key)) return value;
465
+ if (value.length <= truncateStringLength) return value;
466
+ return `${value.slice(0, truncateStringLength)}...[TRUNCATED]`;
467
+ }
468
+ if (Array.isArray(value)) {
469
+ return value.map((item) => truncateValue(item, truncateStringLength));
470
+ }
471
+ if (value && typeof value === "object") {
472
+ return Object.fromEntries(
473
+ Object.entries(value).map(([childKey, childValue]) => [
474
+ childKey,
475
+ truncateValue(childValue, truncateStringLength, childKey)
476
+ ])
477
+ );
478
+ }
479
+ return value;
480
+ }
481
+ function markPayloadTruncated(payload) {
482
+ if (payload.metaData && typeof payload.metaData === "object" && !Array.isArray(payload.metaData)) {
483
+ return {
484
+ ...payload,
485
+ metaData: {
486
+ ...payload.metaData,
487
+ payloadTruncated: true
488
+ }
489
+ };
490
+ }
491
+ return {
492
+ ...payload,
493
+ payloadTruncated: true
494
+ };
495
+ }
496
+ function truncatePayload(payload, truncateStringLength) {
497
+ const cloned = clonePayload(payload);
498
+ if (!cloned) return void 0;
499
+ return markPayloadTruncated(truncateValue(cloned, truncateStringLength));
500
+ }
501
+ function getPayloadMessageId(payload) {
502
+ const value = payload.metaData?.requestId ?? payload.messageId;
503
+ return value === void 0 ? void 0 : String(value);
504
+ }
505
+ function preparePayloadForSizePolicy(payload, options = {}) {
506
+ const maxPayloadBytes = options.maxPayloadBytes ?? 64e3;
507
+ const payloadSizePolicy = options.payloadSizePolicy ?? "drop";
508
+ const payloadTruncateStringLength = options.payloadTruncateStringLength ?? 1024;
509
+ if (payloadByteLength(payload) <= maxPayloadBytes) {
510
+ return { ok: true, payload, truncated: false };
511
+ }
512
+ if (payloadSizePolicy === "truncate") {
513
+ const truncatedPayload = truncatePayload(payload, payloadTruncateStringLength);
514
+ if (truncatedPayload && payloadByteLength(truncatedPayload) <= maxPayloadBytes) {
515
+ return { ok: true, payload: truncatedPayload, truncated: true };
516
+ }
517
+ }
518
+ const messageId = getPayloadMessageId(payload);
519
+ return messageId === void 0 ? { ok: false, reason: "payload_too_large" } : { ok: false, reason: "payload_too_large", messageId };
520
+ }
521
+
106
522
  // src/browser/core/delivery.ts
107
523
  function createId(prefix) {
108
524
  return `${prefix}_${Math.random().toString(36).slice(2)}${Date.now().toString(36)}`;
@@ -134,7 +550,6 @@ function createDeliveryManager(config) {
134
550
  const lockTtlMs = config.lockTtlMs ?? 5e3;
135
551
  const tabId = config.tabId ?? createId("tab");
136
552
  const batchSize = config.batchSize ?? 25;
137
- const maxPayloadBytes = config.maxPayloadBytes ?? 64e3;
138
553
  function recordDrop(event) {
139
554
  diagnostics.dropped.push(event);
140
555
  config.onDrop?.(event);
@@ -143,12 +558,22 @@ function createDeliveryManager(config) {
143
558
  diagnostics.lastError = error.message;
144
559
  config.onError?.(error);
145
560
  }
146
- function payloadByteLength(payload) {
147
- const serialized = JSON.stringify(payload);
148
- if (typeof TextEncoder !== "undefined") {
149
- return new TextEncoder().encode(serialized).length;
561
+ function preparePayloadForSend(payload) {
562
+ const sizePolicyOptions = {};
563
+ if (config.maxPayloadBytes !== void 0) sizePolicyOptions.maxPayloadBytes = config.maxPayloadBytes;
564
+ if (config.payloadSizePolicy !== void 0) sizePolicyOptions.payloadSizePolicy = config.payloadSizePolicy;
565
+ if (config.payloadTruncateStringLength !== void 0) {
566
+ sizePolicyOptions.payloadTruncateStringLength = config.payloadTruncateStringLength;
150
567
  }
151
- return serialized.length;
568
+ const prepared = preparePayloadForSizePolicy(payload, sizePolicyOptions);
569
+ if (prepared.ok) return prepared.payload;
570
+ const diagnostic = {
571
+ reason: prepared.reason,
572
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
573
+ };
574
+ if (prepared.messageId !== void 0) diagnostic.messageId = prepared.messageId;
575
+ recordDrop(diagnostic);
576
+ return void 0;
152
577
  }
153
578
  function recordStorageUnavailable() {
154
579
  recordDrop({
@@ -238,7 +663,19 @@ function createDeliveryManager(config) {
238
663
  idempotencyKey: String(payload.idempotencyKey ?? createIdempotencyKey(payload, messageId))
239
664
  };
240
665
  }
666
+ function tryBeacon(body) {
667
+ if (!config.useBeacon) return false;
668
+ const sendBeacon = globalThis.navigator?.sendBeacon;
669
+ if (typeof sendBeacon !== "function") return false;
670
+ try {
671
+ const blob = new Blob([JSON.stringify(body)], { type: "application/json" });
672
+ return sendBeacon.call(globalThis.navigator, config.endpoint, blob);
673
+ } catch {
674
+ return false;
675
+ }
676
+ }
241
677
  async function deliver(body) {
678
+ if (tryBeacon(body)) return;
242
679
  const fetchImpl = config.fetch ?? globalThis.fetch;
243
680
  if (typeof fetchImpl !== "function") {
244
681
  throw new Error("fetch_unavailable");
@@ -255,26 +692,22 @@ function createDeliveryManager(config) {
255
692
  return {
256
693
  async send(payload) {
257
694
  const body = withEnvelope(payload);
258
- if (payloadByteLength(body) > maxPayloadBytes) {
259
- recordDrop({
260
- messageId: String(body.metaData?.requestId ?? body.messageId),
261
- reason: "payload_too_large",
262
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
263
- });
695
+ const preparedBody = preparePayloadForSend(body);
696
+ if (!preparedBody) {
264
697
  return;
265
698
  }
266
699
  diagnostics.inFlight += 1;
267
700
  try {
268
- await deliver(body);
701
+ await deliver(preparedBody);
269
702
  } catch (error) {
270
703
  const deliveryError = error instanceof Error ? error : new Error(String(error));
271
704
  recordError(deliveryError);
272
705
  enqueue({
273
- messageId: String(body.metaData?.requestId ?? body.messageId),
706
+ messageId: String(getPayloadMessageId(body)),
274
707
  savedAt: Date.now(),
275
708
  attempts: 1,
276
709
  lastError: deliveryError.message,
277
- payload: body
710
+ payload: preparedBody
278
711
  });
279
712
  } finally {
280
713
  diagnostics.inFlight -= 1;
@@ -387,60 +820,268 @@ function createDeliveryManager(config) {
387
820
  };
388
821
  }
389
822
 
390
- // src/browser/core/privacy.ts
391
- var sensitiveKeys = /* @__PURE__ */ new Set([
392
- "email",
393
- "phone",
394
- "mobile",
395
- "address",
396
- "address1",
397
- "address2",
398
- "first_name",
399
- "last_name",
400
- "name",
401
- "token",
402
- "secret",
403
- "password",
404
- "session",
405
- "cookie"
406
- ]);
407
- function sanitizeValue(value, allow) {
408
- if (Array.isArray(value)) {
409
- return value.map((item) => sanitizeValue(item, allow));
823
+ // src/browser/core/identity.ts
824
+ var ANONYMOUS_ID_STORAGE_KEY = "dmd_anonymous_id";
825
+ var LEGACY_ANONYMOUS_ID_STORAGE_KEY = "anonymousId";
826
+ function getUrlParam(name) {
827
+ const href = getBrowserWindow()?.location?.href;
828
+ if (!href) return null;
829
+ try {
830
+ return new URL(href).searchParams.get(name);
831
+ } catch {
832
+ return null;
410
833
  }
411
- if (value && typeof value === "object") {
412
- return Object.fromEntries(
413
- Object.entries(value).filter(([key]) => !sensitiveKeys.has(key.toLowerCase()) || allow.has(key.toLowerCase())).map(([key, nestedValue]) => [key, sanitizeValue(nestedValue, allow)])
414
- );
834
+ }
835
+ function resolveAnonymousId(persistence) {
836
+ const urlAnonymousId = getUrlParam("aid");
837
+ if (isUuid(urlAnonymousId)) {
838
+ persistence.setItem(ANONYMOUS_ID_STORAGE_KEY, urlAnonymousId);
839
+ return urlAnonymousId;
415
840
  }
416
- return value;
841
+ const storedAnonymousId = persistence.getItem(ANONYMOUS_ID_STORAGE_KEY);
842
+ if (isUuid(storedAnonymousId)) {
843
+ return storedAnonymousId;
844
+ }
845
+ const legacyAnonymousId = persistence.getItem(LEGACY_ANONYMOUS_ID_STORAGE_KEY);
846
+ if (isUuid(legacyAnonymousId)) {
847
+ persistence.setItem(ANONYMOUS_ID_STORAGE_KEY, legacyAnonymousId);
848
+ return legacyAnonymousId;
849
+ }
850
+ const anonymousId = createUuid();
851
+ persistence.setItem(ANONYMOUS_ID_STORAGE_KEY, anonymousId);
852
+ return anonymousId;
417
853
  }
418
- function sanitizeProperties(input, allowRawKeys = []) {
419
- const allow = new Set(allowRawKeys.map((key) => key.toLowerCase()));
420
- return sanitizeValue(input, allow);
854
+ function resetAnonymousId(persistence) {
855
+ const anonymousId = createUuid();
856
+ persistence.setItem(ANONYMOUS_ID_STORAGE_KEY, anonymousId);
857
+ return anonymousId;
421
858
  }
422
859
 
423
- // src/browser/core/schema.ts
424
- var reservedKeys = /* @__PURE__ */ new Set(["messageId", "timestamp", "type", "event", "anonymousId", "userId", "context"]);
425
- function validateEventEnvelope(envelope, options = {}) {
426
- if (options.mode === "off") return { ok: true, errors: [] };
427
- const errors = [];
428
- if (typeof envelope.type !== "string") errors.push("type must be a string");
429
- if (envelope.type === "track" && typeof envelope.event !== "string") {
430
- errors.push("track event must include event name");
860
+ // src/browser/core/persistence.ts
861
+ function createPersistenceHealth(requested) {
862
+ return {
863
+ requested,
864
+ cookieFallbackUsed: false,
865
+ memoryFallbackUsed: requested === "memory",
866
+ failures: []
867
+ };
868
+ }
869
+ function recordFailure(health, backend, operation, error) {
870
+ health.failures.push({
871
+ backend,
872
+ operation,
873
+ message: error instanceof Error ? error.message : String(error)
874
+ });
875
+ if (health.failures.length > 25) {
876
+ health.failures.shift();
431
877
  }
432
- if (typeof envelope.messageId !== "string") errors.push("messageId must be a string");
433
- if (typeof envelope.timestamp !== "string") errors.push("timestamp must be an ISO string");
434
- for (const key of Object.keys(envelope.properties ?? {})) {
435
- if (reservedKeys.has(key)) errors.push(`properties.${key} is reserved`);
878
+ }
879
+ function createMemoryPersistence(health = createPersistenceHealth("memory")) {
880
+ const memory = {};
881
+ return {
882
+ getItem(key) {
883
+ return Object.prototype.hasOwnProperty.call(memory, key) ? memory[key] ?? null : null;
884
+ },
885
+ setItem(key, value) {
886
+ memory[key] = value;
887
+ return true;
888
+ },
889
+ removeItem(key) {
890
+ delete memory[key];
891
+ },
892
+ getHealth() {
893
+ return {
894
+ ...health,
895
+ failures: health.failures.map((failure) => ({ ...failure }))
896
+ };
897
+ }
898
+ };
899
+ }
900
+ function getCookie2(name) {
901
+ const browserWindow = getBrowserWindow();
902
+ const cookie = browserWindow?.document?.cookie;
903
+ if (!cookie) return null;
904
+ const escapedName = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
905
+ const match = new RegExp(`(?:^|; )${escapedName}=([^;]*)`).exec(cookie);
906
+ return match ? decodeURIComponent(match[1] ?? "") : null;
907
+ }
908
+ function setCookie(name, value) {
909
+ const browserWindow = getBrowserWindow();
910
+ if (!browserWindow?.document) return false;
911
+ try {
912
+ const expires = new Date(Date.now() + 365 * 24 * 60 * 60 * 1e3).toUTCString();
913
+ const secure = browserWindow.location?.protocol === "https:" ? "; Secure" : "";
914
+ browserWindow.document.cookie = `${name}=${encodeURIComponent(value)}; expires=${expires}; path=/${secure}; SameSite=Lax`;
915
+ return true;
916
+ } catch {
917
+ return false;
918
+ }
919
+ }
920
+ function removeCookie(name) {
921
+ const browserWindow = getBrowserWindow();
922
+ if (!browserWindow?.document) return;
923
+ browserWindow.document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/`;
924
+ }
925
+ function getLocalStorage() {
926
+ const browserWindow = getBrowserWindow();
927
+ try {
928
+ return browserWindow?.localStorage;
929
+ } catch {
930
+ return void 0;
931
+ }
932
+ }
933
+ function getSessionStorage() {
934
+ const browserWindow = getBrowserWindow();
935
+ try {
936
+ return browserWindow?.sessionStorage;
937
+ } catch {
938
+ return void 0;
436
939
  }
437
- return { ok: errors.length === 0, errors };
940
+ }
941
+ function createBrowserPersistence(mode = "localStorage+cookie") {
942
+ const health = createPersistenceHealth(mode);
943
+ const memory = createMemoryPersistence(health);
944
+ if (mode === "none") {
945
+ return {
946
+ getItem() {
947
+ return null;
948
+ },
949
+ setItem() {
950
+ return false;
951
+ },
952
+ removeItem() {
953
+ },
954
+ getHealth() {
955
+ return {
956
+ ...health,
957
+ failures: health.failures.map((failure) => ({ ...failure }))
958
+ };
959
+ }
960
+ };
961
+ }
962
+ if (mode === "memory") return memory;
963
+ const primary = mode === "sessionStorage" ? getSessionStorage() : getLocalStorage();
964
+ const useCookie = mode === "cookie" || mode === "localStorage+cookie" || !primary;
965
+ return {
966
+ getItem(key) {
967
+ try {
968
+ const stored = primary?.getItem(key);
969
+ if (stored) return stored;
970
+ } catch (error) {
971
+ recordFailure(health, mode === "sessionStorage" ? "sessionStorage" : "localStorage", "get", error);
972
+ }
973
+ if (useCookie) {
974
+ const cookie = getCookie2(key);
975
+ if (cookie) {
976
+ health.cookieFallbackUsed = true;
977
+ return cookie;
978
+ }
979
+ }
980
+ const value = memory.getItem(key);
981
+ if (value !== null) health.memoryFallbackUsed = true;
982
+ return value;
983
+ },
984
+ setItem(key, value) {
985
+ let wrote = false;
986
+ try {
987
+ primary?.setItem(key, value);
988
+ wrote = primary !== void 0;
989
+ } catch (error) {
990
+ recordFailure(health, mode === "sessionStorage" ? "sessionStorage" : "localStorage", "set", error);
991
+ wrote = false;
992
+ }
993
+ if (useCookie) {
994
+ const cookieWrote = setCookie(key, value);
995
+ if (cookieWrote) health.cookieFallbackUsed = true;
996
+ wrote = cookieWrote || wrote;
997
+ }
998
+ if (!wrote) {
999
+ health.memoryFallbackUsed = true;
1000
+ return memory.setItem(key, value);
1001
+ }
1002
+ if (mode === "localStorage+cookie") {
1003
+ memory.setItem(key, value);
1004
+ }
1005
+ return true;
1006
+ },
1007
+ removeItem(key) {
1008
+ try {
1009
+ primary?.removeItem(key);
1010
+ } catch (error) {
1011
+ recordFailure(health, mode === "sessionStorage" ? "sessionStorage" : "localStorage", "remove", error);
1012
+ }
1013
+ if (useCookie) removeCookie(key);
1014
+ memory.removeItem(key);
1015
+ },
1016
+ getHealth() {
1017
+ return {
1018
+ ...health,
1019
+ failures: health.failures.map((failure) => ({ ...failure }))
1020
+ };
1021
+ }
1022
+ };
438
1023
  }
439
1024
 
440
- // src/browser/core/DriveMetaDataSDK.ts
441
- function createId2(prefix) {
442
- return `${prefix}_${Math.random().toString(36).slice(2)}${Date.now().toString(36)}`;
1025
+ // src/browser/core/session.ts
1026
+ var SESSION_STORAGE_KEY = "sessionData";
1027
+ function getUrlParam2(name) {
1028
+ const href = getBrowserWindow()?.location?.href;
1029
+ if (!href) return null;
1030
+ try {
1031
+ return new URL(href).searchParams.get(name);
1032
+ } catch {
1033
+ return null;
1034
+ }
443
1035
  }
1036
+ function readStoredSession(persistence) {
1037
+ const rawSession = persistence.getItem(SESSION_STORAGE_KEY);
1038
+ if (!rawSession) return void 0;
1039
+ try {
1040
+ const parsed = JSON.parse(rawSession);
1041
+ if (isUuid(parsed.sessionId) && typeof parsed.timestamp === "number") {
1042
+ return {
1043
+ sessionId: parsed.sessionId,
1044
+ timestamp: parsed.timestamp
1045
+ };
1046
+ }
1047
+ } catch {
1048
+ persistence.removeItem(SESSION_STORAGE_KEY);
1049
+ }
1050
+ return void 0;
1051
+ }
1052
+ function writeStoredSession(persistence, sessionId, timestamp) {
1053
+ persistence.setItem(SESSION_STORAGE_KEY, JSON.stringify({ sessionId, timestamp }));
1054
+ }
1055
+ function createSessionManager(persistence, idleTimeoutSeconds = 30 * 60) {
1056
+ function resolveSessionId() {
1057
+ const now = Date.now();
1058
+ const urlSessionId = getUrlParam2("sid") ?? getUrlParam2("session_id");
1059
+ if (isUuid(urlSessionId)) {
1060
+ writeStoredSession(persistence, urlSessionId, now);
1061
+ return urlSessionId;
1062
+ }
1063
+ const storedSession = readStoredSession(persistence);
1064
+ if (storedSession && now - storedSession.timestamp <= idleTimeoutSeconds * 1e3) {
1065
+ writeStoredSession(persistence, storedSession.sessionId, now);
1066
+ return storedSession.sessionId;
1067
+ }
1068
+ const sessionId = createUuid();
1069
+ writeStoredSession(persistence, sessionId, now);
1070
+ return sessionId;
1071
+ }
1072
+ return {
1073
+ getSessionId() {
1074
+ return resolveSessionId();
1075
+ },
1076
+ reset() {
1077
+ const sessionId = createUuid();
1078
+ writeStoredSession(persistence, sessionId, Date.now());
1079
+ return sessionId;
1080
+ }
1081
+ };
1082
+ }
1083
+
1084
+ // src/browser/core/DriveMetaDataSDK.ts
444
1085
  function endpointFromConfig(config) {
445
1086
  const host = config.apiHost ?? "https://sdk.drivemetadata.com/v2";
446
1087
  return `${host.replace(/\/$/, "")}/data-collector`;
@@ -451,14 +1092,6 @@ function requireConfigString(value, field) {
451
1092
  }
452
1093
  return value;
453
1094
  }
454
- function getBrowserStorage() {
455
- const browserWindow = getBrowserWindow();
456
- try {
457
- return browserWindow?.localStorage;
458
- } catch {
459
- return void 0;
460
- }
461
- }
462
1095
  var DriveMetaDataSDK = class {
463
1096
  constructor(config) {
464
1097
  this.initialized = true;
@@ -471,30 +1104,35 @@ var DriveMetaDataSDK = class {
471
1104
  requireConfigString(config.writeKey || config.token, "writeKey or token");
472
1105
  this.config = config;
473
1106
  this.endpoint = endpointFromConfig(config);
474
- const storage = getBrowserStorage();
1107
+ this.persistence = createBrowserPersistence(config.persistence);
1108
+ this.session = createSessionManager(this.persistence, config.sessionIdleTimeoutSeconds);
475
1109
  const deliveryConfig = {
476
- endpoint: this.endpoint
1110
+ endpoint: this.endpoint,
1111
+ storage: this.persistence
477
1112
  };
478
1113
  if (config.delivery?.maxQueueSize !== void 0) deliveryConfig.maxQueueSize = config.delivery.maxQueueSize;
479
1114
  if (config.delivery?.queueTtlMs !== void 0) deliveryConfig.queueTtlMs = config.delivery.queueTtlMs;
480
1115
  if (config.delivery?.retryDelayMs !== void 0) deliveryConfig.retryDelayMs = config.delivery.retryDelayMs;
481
1116
  if (config.delivery?.maxRetryDelayMs !== void 0) deliveryConfig.maxRetryDelayMs = config.delivery.maxRetryDelayMs;
482
1117
  if (config.delivery?.maxPayloadBytes !== void 0) deliveryConfig.maxPayloadBytes = config.delivery.maxPayloadBytes;
1118
+ if (config.delivery?.payloadSizePolicy !== void 0) deliveryConfig.payloadSizePolicy = config.delivery.payloadSizePolicy;
1119
+ if (config.delivery?.payloadTruncateStringLength !== void 0) {
1120
+ deliveryConfig.payloadTruncateStringLength = config.delivery.payloadTruncateStringLength;
1121
+ }
1122
+ if (config.delivery?.useBeacon !== void 0) deliveryConfig.useBeacon = config.delivery.useBeacon;
483
1123
  if (config.delivery?.batchSize !== void 0) deliveryConfig.batchSize = config.delivery.batchSize;
484
1124
  if (config.onDrop !== void 0) deliveryConfig.onDrop = config.onDrop;
485
1125
  if (config.onError !== void 0) deliveryConfig.onError = config.onError;
486
- this.delivery = createDeliveryManager(storage ? {
487
- ...deliveryConfig,
488
- storage
489
- } : deliveryConfig);
1126
+ this.delivery = createDeliveryManager(deliveryConfig);
490
1127
  this.initialRetryDelayMs = config.delivery?.retryDelayMs ?? 1e3;
491
1128
  this.retryDelayMs = this.initialRetryDelayMs;
492
1129
  this.maxRetryDelayMs = config.delivery?.maxRetryDelayMs ?? 3e4;
493
1130
  this.writeKey = config.writeKey || config.token || "";
494
- this.identity = { anonymousId: createId2("anon") };
495
- this.sessionId = createId2("session");
1131
+ this.identity = { anonymousId: resolveAnonymousId(this.persistence) };
1132
+ captureAttributionFromUrl(this.persistence);
496
1133
  this.consentState = normalizeConsent(config.gdprConsent ?? config.consent);
497
1134
  this.gdprConsent = this.consentState.analytics;
1135
+ this.installAutocapture();
498
1136
  if (!config.delivery?.disableLifecycleFlush) {
499
1137
  this.installLifecycleFlush();
500
1138
  }
@@ -534,7 +1172,8 @@ var DriveMetaDataSDK = class {
534
1172
  }
535
1173
  }
536
1174
  reset() {
537
- this.identity = { anonymousId: createId2("anon") };
1175
+ this.identity = { anonymousId: resetAnonymousId(this.persistence) };
1176
+ this.session.reset();
538
1177
  this.queue = [];
539
1178
  this.offline = false;
540
1179
  if (this.retryTimer !== void 0) {
@@ -543,8 +1182,20 @@ var DriveMetaDataSDK = class {
543
1182
  }
544
1183
  this.lifecycleCleanup?.();
545
1184
  this.lifecycleCleanup = void 0;
1185
+ this.autocaptureCleanup?.();
1186
+ this.autocaptureCleanup = void 0;
546
1187
  this.delivery.clearQueue("manual_clear");
547
1188
  }
1189
+ disposeForTests() {
1190
+ if (this.retryTimer !== void 0) {
1191
+ clearTimeout(this.retryTimer);
1192
+ this.retryTimer = void 0;
1193
+ }
1194
+ this.lifecycleCleanup?.();
1195
+ this.lifecycleCleanup = void 0;
1196
+ this.autocaptureCleanup?.();
1197
+ this.autocaptureCleanup = void 0;
1198
+ }
548
1199
  setConsent(consent) {
549
1200
  this.consentState = typeof consent === "object" ? mergeConsent(this.consentState, consent) : normalizeConsent(consent);
550
1201
  this.gdprConsent = this.consentState.analytics;
@@ -559,6 +1210,7 @@ var DriveMetaDataSDK = class {
559
1210
  initialized: this.initialized,
560
1211
  consent: this.gdprConsent,
561
1212
  consentPurposes: this.consentState,
1213
+ persistence: this.persistence.getHealth(),
562
1214
  queueSize: deliveryDiagnostics.queued,
563
1215
  offline: this.offline || deliveryDiagnostics.queued > 0,
564
1216
  droppedEvents: this.droppedEvents + deliveryDiagnostics.dropped.length,
@@ -573,7 +1225,9 @@ var DriveMetaDataSDK = class {
573
1225
  void this.delivery.send(payload).then(() => {
574
1226
  const diagnostics = this.delivery.getDiagnostics();
575
1227
  this.offline = diagnostics.queued > 0;
576
- this.lastError = diagnostics.lastError;
1228
+ if (diagnostics.lastError !== void 0) {
1229
+ this.lastError = diagnostics.lastError;
1230
+ }
577
1231
  if (diagnostics.queued > 0) {
578
1232
  this.scheduleRetryFlush();
579
1233
  }
@@ -605,6 +1259,28 @@ var DriveMetaDataSDK = class {
605
1259
  }
606
1260
  };
607
1261
  }
1262
+ installAutocapture() {
1263
+ const browserWindow = getBrowserWindow();
1264
+ if (!browserWindow) return;
1265
+ if (this.config.autocapture === false) return;
1266
+ const capturePageview = this.config.capturePageview !== false;
1267
+ const capturePageleave = this.config.capturePageleave !== false;
1268
+ const controller = installAutocapture({
1269
+ browserWindow,
1270
+ capturePageview,
1271
+ capturePageleave,
1272
+ onPageView: () => {
1273
+ this.page();
1274
+ },
1275
+ onPageLeave: () => {
1276
+ this.trackEvent("page_leave", { url: browserWindow.location.href });
1277
+ },
1278
+ onRouteChange: () => {
1279
+ captureAttributionFromUrl(this.persistence);
1280
+ }
1281
+ });
1282
+ this.autocaptureCleanup = controller.cleanup;
1283
+ }
608
1284
  scheduleRetryFlush() {
609
1285
  if (this.retryTimer !== void 0) return;
610
1286
  const delay = Math.min(this.retryDelayMs, this.maxRetryDelayMs);
@@ -630,7 +1306,7 @@ var DriveMetaDataSDK = class {
630
1306
  type,
631
1307
  event,
632
1308
  properties: sanitizeProperties(properties),
633
- messageId: options.messageId ?? createId2("msg"),
1309
+ messageId: ensureUuid(options.messageId),
634
1310
  timestamp: options.timestamp ?? (/* @__PURE__ */ new Date()).toISOString(),
635
1311
  context: options.context ?? {},
636
1312
  anonymousId: this.identity.anonymousId,
@@ -639,7 +1315,7 @@ var DriveMetaDataSDK = class {
639
1315
  appId: this.config.appId,
640
1316
  writeKey: this.writeKey,
641
1317
  consent: this.consentState,
642
- sessionId: this.sessionId
1318
+ sessionId: this.session.getSessionId()
643
1319
  };
644
1320
  if (this.identity.userId !== void 0) prepared.userId = this.identity.userId;
645
1321
  if (this.identity.groupId !== void 0) prepared.groupId = this.identity.groupId;
@@ -663,7 +1339,17 @@ var DriveMetaDataSDK = class {
663
1339
  if (!validation.ok) {
664
1340
  this.lastError = `DMD SDK schema warning: ${validation.errors.join(", ")}`;
665
1341
  }
666
- this.sendEvent(this.toCollectorPayload(prepared));
1342
+ const collectorPayload = this.toCollectorPayload(prepared);
1343
+ const backendValidation = validateBackendCollectorPayload(collectorPayload);
1344
+ if (!backendValidation.ok && this.config.schemaValidation === "strict") {
1345
+ this.lastError = `DMD SDK backend schema warning: ${backendValidation.errors.join(", ")}`;
1346
+ this.recordDrop(type, "invalid_payload", event);
1347
+ return;
1348
+ }
1349
+ if (!backendValidation.ok) {
1350
+ this.lastError = `DMD SDK backend schema warning: ${backendValidation.errors.join(", ")}`;
1351
+ }
1352
+ this.sendEvent(collectorPayload);
667
1353
  }
668
1354
  toCollectorPayload(prepared) {
669
1355
  const browserWindow = getBrowserWindow();
@@ -679,14 +1365,14 @@ var DriveMetaDataSDK = class {
679
1365
  ua: browserWindow?.navigator?.userAgent,
680
1366
  appId: prepared.appId,
681
1367
  pageUrl: browserWindow?.location?.href,
682
- eventData: {
1368
+ eventData: mergeStoredAttribution({
683
1369
  ...prepared.properties,
684
1370
  anonymousId: prepared.anonymousId,
685
1371
  sessionId: prepared.sessionId,
686
1372
  timestamp: prepared.timestamp,
687
1373
  requestSentAt: prepared.timestamp,
688
1374
  requestReceivedAt: prepared.timestamp
689
- }
1375
+ }, this.persistence)
690
1376
  });
691
1377
  }
692
1378
  recordDrop(type, reason, event) {
@@ -762,7 +1448,7 @@ function initDmdSDK(config) {
762
1448
  return publicSingleton;
763
1449
  }
764
1450
  try {
765
- const instance = new DriveMetaDataSDK(config);
1451
+ const instance = new DriveMetaDataSDK(normalizeBrowserConfig(config));
766
1452
  return setSingleton(instance);
767
1453
  } catch (error) {
768
1454
  lastError = error instanceof Error ? error.message : String(error);