@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.
@@ -45,12 +45,62 @@ module.exports = __toCommonJS(next_exports);
45
45
  // src/react/DmdProvider.tsx
46
46
  var import_react = __toESM(require("react"), 1);
47
47
 
48
+ // src/browser/core/browser-config.ts
49
+ function setIfDefined(target, key, value) {
50
+ if (value !== void 0) {
51
+ target[key] = value;
52
+ }
53
+ }
54
+ function normalizeBrowserConfig(input) {
55
+ const normalized = {
56
+ ...input,
57
+ clientId: input.clientId ?? "",
58
+ workspaceId: input.workspaceId ?? "",
59
+ appId: input.appId ?? ""
60
+ };
61
+ setIfDefined(normalized, "apiHost", input.apiHost);
62
+ setIfDefined(normalized, "capturePageview", input.capturePageview);
63
+ setIfDefined(normalized, "capturePageleave", input.capturePageleave);
64
+ setIfDefined(normalized, "captureDeadClicks", input.captureDeadClicks);
65
+ setIfDefined(normalized, "crossSubdomainCookie", input.crossSubdomainCookie);
66
+ setIfDefined(normalized, "sessionIdleTimeoutSeconds", input.sessionIdleTimeoutSeconds);
67
+ setIfDefined(normalized, "schemaValidation", input.schemaValidation);
68
+ setIfDefined(normalized, "beforeSend", input.beforeSend);
69
+ setIfDefined(normalized, "persistence", input.disablePersistence === true ? "none" : input.persistence);
70
+ return normalized;
71
+ }
72
+
73
+ // src/core/uuid.ts
74
+ function createUuid() {
75
+ const cryptoApi = globalThis.crypto;
76
+ if (typeof cryptoApi?.randomUUID === "function") {
77
+ return cryptoApi.randomUUID();
78
+ }
79
+ const bytes = new Uint8Array(16);
80
+ if (typeof cryptoApi?.getRandomValues === "function") {
81
+ cryptoApi.getRandomValues(bytes);
82
+ } else {
83
+ for (let index = 0; index < bytes.length; index += 1) {
84
+ bytes[index] = Math.floor(Math.random() * 256);
85
+ }
86
+ }
87
+ bytes[6] = (bytes[6] ?? 0) & 15 | 64;
88
+ bytes[8] = (bytes[8] ?? 0) & 63 | 128;
89
+ const hex = Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
90
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
91
+ }
92
+ function isUuid(value) {
93
+ 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);
94
+ }
95
+ function ensureUuid(value) {
96
+ return isUuid(value) ? value : createUuid();
97
+ }
98
+
48
99
  // src/core/backend-payload.ts
49
100
  function formatUtcTimestamp(value) {
50
- if (typeof value !== "string") return (/* @__PURE__ */ new Date()).toISOString().replace("T", " ").slice(0, 19);
51
- const date = new Date(value);
52
- if (Number.isNaN(date.getTime())) return value;
53
- return date.toISOString().replace("T", " ").slice(0, 19);
101
+ const date = typeof value === "string" || typeof value === "number" || value instanceof Date ? new Date(value) : /* @__PURE__ */ new Date();
102
+ const safeDate = Number.isNaN(date.getTime()) ? /* @__PURE__ */ new Date() : date;
103
+ return safeDate.toISOString().replace("T", " ").slice(0, 19);
54
104
  }
55
105
  function cleanObject(value) {
56
106
  if (Array.isArray(value)) {
@@ -81,15 +131,19 @@ function createBackendCollectorPayload(input) {
81
131
  };
82
132
  const metaData = {
83
133
  ...normalizedEventData,
84
- requestId: input.requestId,
134
+ requestId: ensureUuid(typeof input.requestId === "string" ? input.requestId : void 0),
85
135
  timestamp,
86
136
  eventType: input.eventType,
87
137
  requestFrom: input.requestFrom ?? "3",
88
138
  clientId: input.clientId,
89
139
  workspaceId: input.workspaceId,
90
140
  token: input.token,
91
- anonymousId: eventData.anonymousId ?? input.anonymousId,
92
- sessionId: eventData.sessionId ?? input.sessionId,
141
+ anonymousId: ensureUuid(
142
+ typeof eventData.anonymousId === "string" ? eventData.anonymousId : typeof input.anonymousId === "string" ? input.anonymousId : void 0
143
+ ),
144
+ sessionId: ensureUuid(
145
+ typeof eventData.sessionId === "string" ? eventData.sessionId : typeof input.sessionId === "string" ? input.sessionId : void 0
146
+ ),
93
147
  ua: input.ua,
94
148
  appDetails: { app_id: input.appId },
95
149
  page: { ...page2, url: page2.url ?? input.pageUrl },
@@ -100,12 +154,45 @@ function createBackendCollectorPayload(input) {
100
154
  return cleanObject(payload);
101
155
  }
102
156
 
103
- // src/core/environment.ts
104
- function getBrowserWindow() {
105
- return typeof window === "undefined" ? void 0 : window;
157
+ // src/core/backend-schema.ts
158
+ var requiredMetaDataFields = [
159
+ "requestId",
160
+ "timestamp",
161
+ "eventType",
162
+ "requestFrom",
163
+ "clientId",
164
+ "workspaceId",
165
+ "anonymousId",
166
+ "sessionId"
167
+ ];
168
+ function isPresent(value) {
169
+ return value !== null && value !== void 0 && value !== "";
170
+ }
171
+ function validateBackendCollectorPayload(payload) {
172
+ const errors = [];
173
+ const metaData = payload.metaData;
174
+ if (!metaData || typeof metaData !== "object" || Array.isArray(metaData)) {
175
+ return {
176
+ ok: false,
177
+ errors: ["metaData is required"]
178
+ };
179
+ }
180
+ const metadataRecord = metaData;
181
+ for (const field of requiredMetaDataFields) {
182
+ if (!isPresent(metadataRecord[field])) {
183
+ errors.push(`metaData.${field} is required`);
184
+ }
185
+ }
186
+ if (isPresent(metadataRecord.eventType) && typeof metadataRecord.eventType !== "string") {
187
+ errors.push("metaData.eventType must be a string");
188
+ }
189
+ return {
190
+ ok: errors.length === 0,
191
+ errors
192
+ };
106
193
  }
107
194
 
108
- // src/browser/core/consent.ts
195
+ // src/core/consent.ts
109
196
  var purposes = [
110
197
  "analytics",
111
198
  "advertising",
@@ -145,6 +232,335 @@ function canCollectPurpose(consent, purpose) {
145
232
  return consent[purpose] === "granted";
146
233
  }
147
234
 
235
+ // src/core/event-schema.ts
236
+ var reservedKeys = /* @__PURE__ */ new Set(["messageId", "timestamp", "type", "event", "anonymousId", "userId", "context"]);
237
+ function validateEventEnvelope(envelope, options = {}) {
238
+ if (options.mode === "off") return { ok: true, errors: [] };
239
+ const errors = [];
240
+ if (typeof envelope.type !== "string") errors.push("type must be a string");
241
+ if (envelope.type === "track" && typeof envelope.event !== "string") {
242
+ errors.push("track event must include event name");
243
+ }
244
+ if (typeof envelope.messageId !== "string") errors.push("messageId must be a string");
245
+ if (typeof envelope.timestamp !== "string") errors.push("timestamp must be an ISO string");
246
+ for (const key of Object.keys(envelope.properties ?? {})) {
247
+ if (reservedKeys.has(key)) errors.push(`properties.${key} is reserved`);
248
+ }
249
+ return { ok: errors.length === 0, errors };
250
+ }
251
+
252
+ // src/core/environment.ts
253
+ function getBrowserWindow() {
254
+ return typeof window === "undefined" ? void 0 : window;
255
+ }
256
+
257
+ // src/core/privacy.ts
258
+ var sensitiveKeys = /* @__PURE__ */ new Set([
259
+ "email",
260
+ "phone",
261
+ "mobile",
262
+ "address",
263
+ "address1",
264
+ "address2",
265
+ "first_name",
266
+ "last_name",
267
+ "name",
268
+ "token",
269
+ "secret",
270
+ "password",
271
+ "session",
272
+ "cookie"
273
+ ]);
274
+ function sanitizeValue(value, allow, path = []) {
275
+ if (Array.isArray(value)) {
276
+ return value.map((item) => sanitizeValue(item, allow, path));
277
+ }
278
+ if (value && typeof value === "object") {
279
+ return Object.fromEntries(
280
+ Object.entries(value).filter(([key]) => {
281
+ const lowerKey = key.toLowerCase();
282
+ const lowerPath = path.map((item) => item.toLowerCase());
283
+ const isEcommerceItemName = lowerKey === "name" && lowerPath.includes("ecommerce") && lowerPath.includes("items");
284
+ return isEcommerceItemName || !sensitiveKeys.has(lowerKey) || allow.has(lowerKey);
285
+ }).map(([key, nestedValue]) => [key, sanitizeValue(nestedValue, allow, [...path, key])])
286
+ );
287
+ }
288
+ return value;
289
+ }
290
+ function sanitizeProperties(input, allowRawKeys = []) {
291
+ const allow = new Set(allowRawKeys.map((key) => key.toLowerCase()));
292
+ return sanitizeValue(input, allow);
293
+ }
294
+
295
+ // src/core/attribution.ts
296
+ var dmdUtmKeys = [
297
+ "utm_source",
298
+ "utm_medium",
299
+ "utm_campaign",
300
+ "utm_term",
301
+ "utm_content",
302
+ "utm_id"
303
+ ];
304
+ var dmdCampaignKeys = ["campaign_id", "ad_id"];
305
+ var dmdClickIdSources = [
306
+ ["gclid", 2],
307
+ ["fbclid", 3],
308
+ ["ScCid", 1],
309
+ ["li_fat_id", 4]
310
+ ];
311
+ function cleanAttributionRecord(record) {
312
+ const cleaned = Object.fromEntries(
313
+ Object.entries(record).filter(([, value]) => value !== null && value !== void 0 && value !== "")
314
+ );
315
+ return Object.keys(cleaned).length > 0 ? cleaned : void 0;
316
+ }
317
+ function objectValue(value) {
318
+ return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
319
+ }
320
+ function mergeAttributionRecords(explicitProperties, stored) {
321
+ const attributionData = objectValue(explicitProperties.attributionData);
322
+ const utmParameter = objectValue(explicitProperties.utmParameter);
323
+ return {
324
+ ...explicitProperties,
325
+ ...stored.attributionData || attributionData ? { attributionData: { ...stored.attributionData ?? {}, ...attributionData ?? {} } } : {},
326
+ ...stored.utmParameter || utmParameter ? { utmParameter: { ...stored.utmParameter ?? {}, ...utmParameter ?? {} } } : {}
327
+ };
328
+ }
329
+
330
+ // src/browser/core/attribution.ts
331
+ function getCurrentUrl() {
332
+ return getBrowserWindow()?.location?.href;
333
+ }
334
+ function getCookie(name) {
335
+ const cookie = getBrowserWindow()?.document?.cookie;
336
+ if (!cookie) return void 0;
337
+ const escapedName = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
338
+ const match = new RegExp(`(?:^|; )${escapedName}=([^;]*)`).exec(cookie);
339
+ return match ? decodeURIComponent(match[1] ?? "") : void 0;
340
+ }
341
+ function getParams(url) {
342
+ return new URL(url, "https://placeholder.local").searchParams;
343
+ }
344
+ function setIfPresent(persistence, key, value) {
345
+ if (value !== null && value !== "") {
346
+ persistence.setItem(key, value);
347
+ }
348
+ }
349
+ function getStoredString(persistence, key) {
350
+ const value = persistence.getItem(key);
351
+ return value === null || value === "" ? void 0 : value;
352
+ }
353
+ function getStoredNumberOrString(persistence, key) {
354
+ const value = getStoredString(persistence, key);
355
+ if (value === void 0) return void 0;
356
+ const parsed = Number.parseInt(value, 10);
357
+ return Number.isNaN(parsed) ? value : parsed;
358
+ }
359
+ function captureAttributionFromUrl(persistence, url = getCurrentUrl()) {
360
+ if (!url) return;
361
+ const params = getParams(url);
362
+ for (const key of dmdUtmKeys) {
363
+ setIfPresent(persistence, key, params.get(key));
364
+ }
365
+ for (const key of dmdCampaignKeys) {
366
+ setIfPresent(persistence, key, params.get(key));
367
+ }
368
+ for (const [param, sdkPubId] of dmdClickIdSources) {
369
+ const value = params.get(param);
370
+ if (value) {
371
+ persistence.setItem("unique_id", value);
372
+ persistence.setItem("sdk_pub_id", String(sdkPubId));
373
+ }
374
+ }
375
+ }
376
+ function getStoredAttributionData(persistence) {
377
+ return cleanAttributionRecord({
378
+ unique_id: getStoredString(persistence, "unique_id"),
379
+ sdk_pub_id: getStoredNumberOrString(persistence, "sdk_pub_id"),
380
+ fbc: getStoredString(persistence, "fbc") ?? getCookie("_fbc"),
381
+ fbp: getStoredString(persistence, "fbp") ?? getCookie("_fbp"),
382
+ campaign_id: getStoredString(persistence, "campaign_id"),
383
+ ad_id: getStoredString(persistence, "ad_id")
384
+ });
385
+ }
386
+ function getStoredUtmParameter(persistence) {
387
+ return cleanAttributionRecord(Object.fromEntries(dmdUtmKeys.map((key) => [key, getStoredString(persistence, key)])));
388
+ }
389
+ function mergeStoredAttribution(properties, persistence) {
390
+ const stored = {};
391
+ const attributionData = getStoredAttributionData(persistence);
392
+ const utmParameter = getStoredUtmParameter(persistence);
393
+ if (attributionData !== void 0) stored.attributionData = attributionData;
394
+ if (utmParameter !== void 0) stored.utmParameter = utmParameter;
395
+ return mergeAttributionRecords(properties, stored);
396
+ }
397
+
398
+ // src/browser/core/autocapture.ts
399
+ function installAutocapture(config) {
400
+ const trackedPageUrls = /* @__PURE__ */ new Set();
401
+ const timers = /* @__PURE__ */ new Set();
402
+ const pageviewDelayMs = config.pageviewDelayMs ?? 500;
403
+ const history = config.browserWindow.history;
404
+ const originalPushState = history?.pushState;
405
+ const originalReplaceState = history?.replaceState;
406
+ function clearTimer(timer) {
407
+ timers.delete(timer);
408
+ clearTimeout(timer);
409
+ }
410
+ function canonicalUrl() {
411
+ return config.browserWindow.location.href;
412
+ }
413
+ function trackCurrentPageview() {
414
+ if (!config.capturePageview) return;
415
+ const url = canonicalUrl();
416
+ if (trackedPageUrls.has(url)) return;
417
+ trackedPageUrls.add(url);
418
+ config.onPageView();
419
+ }
420
+ function schedulePageview(delayMs) {
421
+ if (!config.capturePageview) return;
422
+ const timer = setTimeout(() => {
423
+ timers.delete(timer);
424
+ trackCurrentPageview();
425
+ }, delayMs);
426
+ timers.add(timer);
427
+ }
428
+ function handleRouteChange() {
429
+ config.onRouteChange?.();
430
+ trackCurrentPageview();
431
+ }
432
+ function wrapHistoryMethod(method) {
433
+ if (!history) return;
434
+ const original = history[method];
435
+ if (typeof original !== "function") return;
436
+ history[method] = function wrappedHistoryMethod(...args) {
437
+ const result = original.apply(this, args);
438
+ const timer = setTimeout(() => {
439
+ timers.delete(timer);
440
+ handleRouteChange();
441
+ }, 0);
442
+ timers.add(timer);
443
+ return result;
444
+ };
445
+ }
446
+ function handlePopOrHashChange() {
447
+ handleRouteChange();
448
+ }
449
+ function handlePageLeave() {
450
+ if (config.capturePageleave) {
451
+ config.onPageLeave();
452
+ }
453
+ }
454
+ schedulePageview(pageviewDelayMs);
455
+ wrapHistoryMethod("pushState");
456
+ wrapHistoryMethod("replaceState");
457
+ const canListen = typeof config.browserWindow.addEventListener === "function" && typeof config.browserWindow.removeEventListener === "function";
458
+ if (canListen) {
459
+ config.browserWindow.addEventListener("popstate", handlePopOrHashChange);
460
+ config.browserWindow.addEventListener("hashchange", handlePopOrHashChange);
461
+ config.browserWindow.addEventListener("beforeunload", handlePageLeave);
462
+ }
463
+ return {
464
+ cleanup() {
465
+ for (const timer of Array.from(timers)) {
466
+ clearTimer(timer);
467
+ }
468
+ if (history && originalPushState) history.pushState = originalPushState;
469
+ if (history && originalReplaceState) history.replaceState = originalReplaceState;
470
+ if (canListen) {
471
+ config.browserWindow.removeEventListener("popstate", handlePopOrHashChange);
472
+ config.browserWindow.removeEventListener("hashchange", handlePopOrHashChange);
473
+ config.browserWindow.removeEventListener("beforeunload", handlePageLeave);
474
+ }
475
+ }
476
+ };
477
+ }
478
+
479
+ // src/core/payload-size.ts
480
+ var preserveStringKeys = /* @__PURE__ */ new Set([
481
+ "requestId",
482
+ "eventType",
483
+ "requestFrom",
484
+ "clientId",
485
+ "workspaceId",
486
+ "anonymousId",
487
+ "sessionId",
488
+ "token"
489
+ ]);
490
+ function payloadByteLength(payload) {
491
+ const serialized = JSON.stringify(payload);
492
+ if (typeof TextEncoder !== "undefined") {
493
+ return new TextEncoder().encode(serialized).length;
494
+ }
495
+ return serialized.length;
496
+ }
497
+ function clonePayload(payload) {
498
+ try {
499
+ return JSON.parse(JSON.stringify(payload));
500
+ } catch {
501
+ return void 0;
502
+ }
503
+ }
504
+ function truncateValue(value, truncateStringLength, key) {
505
+ if (typeof value === "string") {
506
+ if (key && preserveStringKeys.has(key)) return value;
507
+ if (value.length <= truncateStringLength) return value;
508
+ return `${value.slice(0, truncateStringLength)}...[TRUNCATED]`;
509
+ }
510
+ if (Array.isArray(value)) {
511
+ return value.map((item) => truncateValue(item, truncateStringLength));
512
+ }
513
+ if (value && typeof value === "object") {
514
+ return Object.fromEntries(
515
+ Object.entries(value).map(([childKey, childValue]) => [
516
+ childKey,
517
+ truncateValue(childValue, truncateStringLength, childKey)
518
+ ])
519
+ );
520
+ }
521
+ return value;
522
+ }
523
+ function markPayloadTruncated(payload) {
524
+ if (payload.metaData && typeof payload.metaData === "object" && !Array.isArray(payload.metaData)) {
525
+ return {
526
+ ...payload,
527
+ metaData: {
528
+ ...payload.metaData,
529
+ payloadTruncated: true
530
+ }
531
+ };
532
+ }
533
+ return {
534
+ ...payload,
535
+ payloadTruncated: true
536
+ };
537
+ }
538
+ function truncatePayload(payload, truncateStringLength) {
539
+ const cloned = clonePayload(payload);
540
+ if (!cloned) return void 0;
541
+ return markPayloadTruncated(truncateValue(cloned, truncateStringLength));
542
+ }
543
+ function getPayloadMessageId(payload) {
544
+ const value = payload.metaData?.requestId ?? payload.messageId;
545
+ return value === void 0 ? void 0 : String(value);
546
+ }
547
+ function preparePayloadForSizePolicy(payload, options = {}) {
548
+ const maxPayloadBytes = options.maxPayloadBytes ?? 64e3;
549
+ const payloadSizePolicy = options.payloadSizePolicy ?? "drop";
550
+ const payloadTruncateStringLength = options.payloadTruncateStringLength ?? 1024;
551
+ if (payloadByteLength(payload) <= maxPayloadBytes) {
552
+ return { ok: true, payload, truncated: false };
553
+ }
554
+ if (payloadSizePolicy === "truncate") {
555
+ const truncatedPayload = truncatePayload(payload, payloadTruncateStringLength);
556
+ if (truncatedPayload && payloadByteLength(truncatedPayload) <= maxPayloadBytes) {
557
+ return { ok: true, payload: truncatedPayload, truncated: true };
558
+ }
559
+ }
560
+ const messageId = getPayloadMessageId(payload);
561
+ return messageId === void 0 ? { ok: false, reason: "payload_too_large" } : { ok: false, reason: "payload_too_large", messageId };
562
+ }
563
+
148
564
  // src/browser/core/delivery.ts
149
565
  function createId(prefix) {
150
566
  return `${prefix}_${Math.random().toString(36).slice(2)}${Date.now().toString(36)}`;
@@ -176,7 +592,6 @@ function createDeliveryManager(config) {
176
592
  const lockTtlMs = config.lockTtlMs ?? 5e3;
177
593
  const tabId = config.tabId ?? createId("tab");
178
594
  const batchSize = config.batchSize ?? 25;
179
- const maxPayloadBytes = config.maxPayloadBytes ?? 64e3;
180
595
  function recordDrop(event) {
181
596
  diagnostics.dropped.push(event);
182
597
  config.onDrop?.(event);
@@ -185,12 +600,22 @@ function createDeliveryManager(config) {
185
600
  diagnostics.lastError = error.message;
186
601
  config.onError?.(error);
187
602
  }
188
- function payloadByteLength(payload) {
189
- const serialized = JSON.stringify(payload);
190
- if (typeof TextEncoder !== "undefined") {
191
- return new TextEncoder().encode(serialized).length;
603
+ function preparePayloadForSend(payload) {
604
+ const sizePolicyOptions = {};
605
+ if (config.maxPayloadBytes !== void 0) sizePolicyOptions.maxPayloadBytes = config.maxPayloadBytes;
606
+ if (config.payloadSizePolicy !== void 0) sizePolicyOptions.payloadSizePolicy = config.payloadSizePolicy;
607
+ if (config.payloadTruncateStringLength !== void 0) {
608
+ sizePolicyOptions.payloadTruncateStringLength = config.payloadTruncateStringLength;
192
609
  }
193
- return serialized.length;
610
+ const prepared = preparePayloadForSizePolicy(payload, sizePolicyOptions);
611
+ if (prepared.ok) return prepared.payload;
612
+ const diagnostic = {
613
+ reason: prepared.reason,
614
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
615
+ };
616
+ if (prepared.messageId !== void 0) diagnostic.messageId = prepared.messageId;
617
+ recordDrop(diagnostic);
618
+ return void 0;
194
619
  }
195
620
  function recordStorageUnavailable() {
196
621
  recordDrop({
@@ -280,7 +705,19 @@ function createDeliveryManager(config) {
280
705
  idempotencyKey: String(payload.idempotencyKey ?? createIdempotencyKey(payload, messageId))
281
706
  };
282
707
  }
708
+ function tryBeacon(body) {
709
+ if (!config.useBeacon) return false;
710
+ const sendBeacon = globalThis.navigator?.sendBeacon;
711
+ if (typeof sendBeacon !== "function") return false;
712
+ try {
713
+ const blob = new Blob([JSON.stringify(body)], { type: "application/json" });
714
+ return sendBeacon.call(globalThis.navigator, config.endpoint, blob);
715
+ } catch {
716
+ return false;
717
+ }
718
+ }
283
719
  async function deliver(body) {
720
+ if (tryBeacon(body)) return;
284
721
  const fetchImpl = config.fetch ?? globalThis.fetch;
285
722
  if (typeof fetchImpl !== "function") {
286
723
  throw new Error("fetch_unavailable");
@@ -297,26 +734,22 @@ function createDeliveryManager(config) {
297
734
  return {
298
735
  async send(payload) {
299
736
  const body = withEnvelope(payload);
300
- if (payloadByteLength(body) > maxPayloadBytes) {
301
- recordDrop({
302
- messageId: String(body.metaData?.requestId ?? body.messageId),
303
- reason: "payload_too_large",
304
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
305
- });
737
+ const preparedBody = preparePayloadForSend(body);
738
+ if (!preparedBody) {
306
739
  return;
307
740
  }
308
741
  diagnostics.inFlight += 1;
309
742
  try {
310
- await deliver(body);
743
+ await deliver(preparedBody);
311
744
  } catch (error) {
312
745
  const deliveryError = error instanceof Error ? error : new Error(String(error));
313
746
  recordError(deliveryError);
314
747
  enqueue({
315
- messageId: String(body.metaData?.requestId ?? body.messageId),
748
+ messageId: String(getPayloadMessageId(body)),
316
749
  savedAt: Date.now(),
317
750
  attempts: 1,
318
751
  lastError: deliveryError.message,
319
- payload: body
752
+ payload: preparedBody
320
753
  });
321
754
  } finally {
322
755
  diagnostics.inFlight -= 1;
@@ -429,60 +862,268 @@ function createDeliveryManager(config) {
429
862
  };
430
863
  }
431
864
 
432
- // src/browser/core/privacy.ts
433
- var sensitiveKeys = /* @__PURE__ */ new Set([
434
- "email",
435
- "phone",
436
- "mobile",
437
- "address",
438
- "address1",
439
- "address2",
440
- "first_name",
441
- "last_name",
442
- "name",
443
- "token",
444
- "secret",
445
- "password",
446
- "session",
447
- "cookie"
448
- ]);
449
- function sanitizeValue(value, allow) {
450
- if (Array.isArray(value)) {
451
- return value.map((item) => sanitizeValue(item, allow));
865
+ // src/browser/core/identity.ts
866
+ var ANONYMOUS_ID_STORAGE_KEY = "dmd_anonymous_id";
867
+ var LEGACY_ANONYMOUS_ID_STORAGE_KEY = "anonymousId";
868
+ function getUrlParam(name) {
869
+ const href = getBrowserWindow()?.location?.href;
870
+ if (!href) return null;
871
+ try {
872
+ return new URL(href).searchParams.get(name);
873
+ } catch {
874
+ return null;
452
875
  }
453
- if (value && typeof value === "object") {
454
- return Object.fromEntries(
455
- Object.entries(value).filter(([key]) => !sensitiveKeys.has(key.toLowerCase()) || allow.has(key.toLowerCase())).map(([key, nestedValue]) => [key, sanitizeValue(nestedValue, allow)])
456
- );
876
+ }
877
+ function resolveAnonymousId(persistence) {
878
+ const urlAnonymousId = getUrlParam("aid");
879
+ if (isUuid(urlAnonymousId)) {
880
+ persistence.setItem(ANONYMOUS_ID_STORAGE_KEY, urlAnonymousId);
881
+ return urlAnonymousId;
457
882
  }
458
- return value;
883
+ const storedAnonymousId = persistence.getItem(ANONYMOUS_ID_STORAGE_KEY);
884
+ if (isUuid(storedAnonymousId)) {
885
+ return storedAnonymousId;
886
+ }
887
+ const legacyAnonymousId = persistence.getItem(LEGACY_ANONYMOUS_ID_STORAGE_KEY);
888
+ if (isUuid(legacyAnonymousId)) {
889
+ persistence.setItem(ANONYMOUS_ID_STORAGE_KEY, legacyAnonymousId);
890
+ return legacyAnonymousId;
891
+ }
892
+ const anonymousId = createUuid();
893
+ persistence.setItem(ANONYMOUS_ID_STORAGE_KEY, anonymousId);
894
+ return anonymousId;
459
895
  }
460
- function sanitizeProperties(input, allowRawKeys = []) {
461
- const allow = new Set(allowRawKeys.map((key) => key.toLowerCase()));
462
- return sanitizeValue(input, allow);
896
+ function resetAnonymousId(persistence) {
897
+ const anonymousId = createUuid();
898
+ persistence.setItem(ANONYMOUS_ID_STORAGE_KEY, anonymousId);
899
+ return anonymousId;
463
900
  }
464
901
 
465
- // src/browser/core/schema.ts
466
- var reservedKeys = /* @__PURE__ */ new Set(["messageId", "timestamp", "type", "event", "anonymousId", "userId", "context"]);
467
- function validateEventEnvelope(envelope, options = {}) {
468
- if (options.mode === "off") return { ok: true, errors: [] };
469
- const errors = [];
470
- if (typeof envelope.type !== "string") errors.push("type must be a string");
471
- if (envelope.type === "track" && typeof envelope.event !== "string") {
472
- errors.push("track event must include event name");
902
+ // src/browser/core/persistence.ts
903
+ function createPersistenceHealth(requested) {
904
+ return {
905
+ requested,
906
+ cookieFallbackUsed: false,
907
+ memoryFallbackUsed: requested === "memory",
908
+ failures: []
909
+ };
910
+ }
911
+ function recordFailure(health, backend, operation, error) {
912
+ health.failures.push({
913
+ backend,
914
+ operation,
915
+ message: error instanceof Error ? error.message : String(error)
916
+ });
917
+ if (health.failures.length > 25) {
918
+ health.failures.shift();
473
919
  }
474
- if (typeof envelope.messageId !== "string") errors.push("messageId must be a string");
475
- if (typeof envelope.timestamp !== "string") errors.push("timestamp must be an ISO string");
476
- for (const key of Object.keys(envelope.properties ?? {})) {
477
- if (reservedKeys.has(key)) errors.push(`properties.${key} is reserved`);
920
+ }
921
+ function createMemoryPersistence(health = createPersistenceHealth("memory")) {
922
+ const memory = {};
923
+ return {
924
+ getItem(key) {
925
+ return Object.prototype.hasOwnProperty.call(memory, key) ? memory[key] ?? null : null;
926
+ },
927
+ setItem(key, value) {
928
+ memory[key] = value;
929
+ return true;
930
+ },
931
+ removeItem(key) {
932
+ delete memory[key];
933
+ },
934
+ getHealth() {
935
+ return {
936
+ ...health,
937
+ failures: health.failures.map((failure) => ({ ...failure }))
938
+ };
939
+ }
940
+ };
941
+ }
942
+ function getCookie2(name) {
943
+ const browserWindow = getBrowserWindow();
944
+ const cookie = browserWindow?.document?.cookie;
945
+ if (!cookie) return null;
946
+ const escapedName = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
947
+ const match = new RegExp(`(?:^|; )${escapedName}=([^;]*)`).exec(cookie);
948
+ return match ? decodeURIComponent(match[1] ?? "") : null;
949
+ }
950
+ function setCookie(name, value) {
951
+ const browserWindow = getBrowserWindow();
952
+ if (!browserWindow?.document) return false;
953
+ try {
954
+ const expires = new Date(Date.now() + 365 * 24 * 60 * 60 * 1e3).toUTCString();
955
+ const secure = browserWindow.location?.protocol === "https:" ? "; Secure" : "";
956
+ browserWindow.document.cookie = `${name}=${encodeURIComponent(value)}; expires=${expires}; path=/${secure}; SameSite=Lax`;
957
+ return true;
958
+ } catch {
959
+ return false;
960
+ }
961
+ }
962
+ function removeCookie(name) {
963
+ const browserWindow = getBrowserWindow();
964
+ if (!browserWindow?.document) return;
965
+ browserWindow.document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/`;
966
+ }
967
+ function getLocalStorage() {
968
+ const browserWindow = getBrowserWindow();
969
+ try {
970
+ return browserWindow?.localStorage;
971
+ } catch {
972
+ return void 0;
973
+ }
974
+ }
975
+ function getSessionStorage() {
976
+ const browserWindow = getBrowserWindow();
977
+ try {
978
+ return browserWindow?.sessionStorage;
979
+ } catch {
980
+ return void 0;
478
981
  }
479
- return { ok: errors.length === 0, errors };
982
+ }
983
+ function createBrowserPersistence(mode = "localStorage+cookie") {
984
+ const health = createPersistenceHealth(mode);
985
+ const memory = createMemoryPersistence(health);
986
+ if (mode === "none") {
987
+ return {
988
+ getItem() {
989
+ return null;
990
+ },
991
+ setItem() {
992
+ return false;
993
+ },
994
+ removeItem() {
995
+ },
996
+ getHealth() {
997
+ return {
998
+ ...health,
999
+ failures: health.failures.map((failure) => ({ ...failure }))
1000
+ };
1001
+ }
1002
+ };
1003
+ }
1004
+ if (mode === "memory") return memory;
1005
+ const primary = mode === "sessionStorage" ? getSessionStorage() : getLocalStorage();
1006
+ const useCookie = mode === "cookie" || mode === "localStorage+cookie" || !primary;
1007
+ return {
1008
+ getItem(key) {
1009
+ try {
1010
+ const stored = primary?.getItem(key);
1011
+ if (stored) return stored;
1012
+ } catch (error) {
1013
+ recordFailure(health, mode === "sessionStorage" ? "sessionStorage" : "localStorage", "get", error);
1014
+ }
1015
+ if (useCookie) {
1016
+ const cookie = getCookie2(key);
1017
+ if (cookie) {
1018
+ health.cookieFallbackUsed = true;
1019
+ return cookie;
1020
+ }
1021
+ }
1022
+ const value = memory.getItem(key);
1023
+ if (value !== null) health.memoryFallbackUsed = true;
1024
+ return value;
1025
+ },
1026
+ setItem(key, value) {
1027
+ let wrote = false;
1028
+ try {
1029
+ primary?.setItem(key, value);
1030
+ wrote = primary !== void 0;
1031
+ } catch (error) {
1032
+ recordFailure(health, mode === "sessionStorage" ? "sessionStorage" : "localStorage", "set", error);
1033
+ wrote = false;
1034
+ }
1035
+ if (useCookie) {
1036
+ const cookieWrote = setCookie(key, value);
1037
+ if (cookieWrote) health.cookieFallbackUsed = true;
1038
+ wrote = cookieWrote || wrote;
1039
+ }
1040
+ if (!wrote) {
1041
+ health.memoryFallbackUsed = true;
1042
+ return memory.setItem(key, value);
1043
+ }
1044
+ if (mode === "localStorage+cookie") {
1045
+ memory.setItem(key, value);
1046
+ }
1047
+ return true;
1048
+ },
1049
+ removeItem(key) {
1050
+ try {
1051
+ primary?.removeItem(key);
1052
+ } catch (error) {
1053
+ recordFailure(health, mode === "sessionStorage" ? "sessionStorage" : "localStorage", "remove", error);
1054
+ }
1055
+ if (useCookie) removeCookie(key);
1056
+ memory.removeItem(key);
1057
+ },
1058
+ getHealth() {
1059
+ return {
1060
+ ...health,
1061
+ failures: health.failures.map((failure) => ({ ...failure }))
1062
+ };
1063
+ }
1064
+ };
480
1065
  }
481
1066
 
482
- // src/browser/core/DriveMetaDataSDK.ts
483
- function createId2(prefix) {
484
- return `${prefix}_${Math.random().toString(36).slice(2)}${Date.now().toString(36)}`;
1067
+ // src/browser/core/session.ts
1068
+ var SESSION_STORAGE_KEY = "sessionData";
1069
+ function getUrlParam2(name) {
1070
+ const href = getBrowserWindow()?.location?.href;
1071
+ if (!href) return null;
1072
+ try {
1073
+ return new URL(href).searchParams.get(name);
1074
+ } catch {
1075
+ return null;
1076
+ }
485
1077
  }
1078
+ function readStoredSession(persistence) {
1079
+ const rawSession = persistence.getItem(SESSION_STORAGE_KEY);
1080
+ if (!rawSession) return void 0;
1081
+ try {
1082
+ const parsed = JSON.parse(rawSession);
1083
+ if (isUuid(parsed.sessionId) && typeof parsed.timestamp === "number") {
1084
+ return {
1085
+ sessionId: parsed.sessionId,
1086
+ timestamp: parsed.timestamp
1087
+ };
1088
+ }
1089
+ } catch {
1090
+ persistence.removeItem(SESSION_STORAGE_KEY);
1091
+ }
1092
+ return void 0;
1093
+ }
1094
+ function writeStoredSession(persistence, sessionId, timestamp) {
1095
+ persistence.setItem(SESSION_STORAGE_KEY, JSON.stringify({ sessionId, timestamp }));
1096
+ }
1097
+ function createSessionManager(persistence, idleTimeoutSeconds = 30 * 60) {
1098
+ function resolveSessionId() {
1099
+ const now = Date.now();
1100
+ const urlSessionId = getUrlParam2("sid") ?? getUrlParam2("session_id");
1101
+ if (isUuid(urlSessionId)) {
1102
+ writeStoredSession(persistence, urlSessionId, now);
1103
+ return urlSessionId;
1104
+ }
1105
+ const storedSession = readStoredSession(persistence);
1106
+ if (storedSession && now - storedSession.timestamp <= idleTimeoutSeconds * 1e3) {
1107
+ writeStoredSession(persistence, storedSession.sessionId, now);
1108
+ return storedSession.sessionId;
1109
+ }
1110
+ const sessionId = createUuid();
1111
+ writeStoredSession(persistence, sessionId, now);
1112
+ return sessionId;
1113
+ }
1114
+ return {
1115
+ getSessionId() {
1116
+ return resolveSessionId();
1117
+ },
1118
+ reset() {
1119
+ const sessionId = createUuid();
1120
+ writeStoredSession(persistence, sessionId, Date.now());
1121
+ return sessionId;
1122
+ }
1123
+ };
1124
+ }
1125
+
1126
+ // src/browser/core/DriveMetaDataSDK.ts
486
1127
  function endpointFromConfig(config) {
487
1128
  const host = config.apiHost ?? "https://sdk.drivemetadata.com/v2";
488
1129
  return `${host.replace(/\/$/, "")}/data-collector`;
@@ -493,14 +1134,6 @@ function requireConfigString(value, field) {
493
1134
  }
494
1135
  return value;
495
1136
  }
496
- function getBrowserStorage() {
497
- const browserWindow = getBrowserWindow();
498
- try {
499
- return browserWindow?.localStorage;
500
- } catch {
501
- return void 0;
502
- }
503
- }
504
1137
  var DriveMetaDataSDK = class {
505
1138
  constructor(config) {
506
1139
  this.initialized = true;
@@ -513,30 +1146,35 @@ var DriveMetaDataSDK = class {
513
1146
  requireConfigString(config.writeKey || config.token, "writeKey or token");
514
1147
  this.config = config;
515
1148
  this.endpoint = endpointFromConfig(config);
516
- const storage = getBrowserStorage();
1149
+ this.persistence = createBrowserPersistence(config.persistence);
1150
+ this.session = createSessionManager(this.persistence, config.sessionIdleTimeoutSeconds);
517
1151
  const deliveryConfig = {
518
- endpoint: this.endpoint
1152
+ endpoint: this.endpoint,
1153
+ storage: this.persistence
519
1154
  };
520
1155
  if (config.delivery?.maxQueueSize !== void 0) deliveryConfig.maxQueueSize = config.delivery.maxQueueSize;
521
1156
  if (config.delivery?.queueTtlMs !== void 0) deliveryConfig.queueTtlMs = config.delivery.queueTtlMs;
522
1157
  if (config.delivery?.retryDelayMs !== void 0) deliveryConfig.retryDelayMs = config.delivery.retryDelayMs;
523
1158
  if (config.delivery?.maxRetryDelayMs !== void 0) deliveryConfig.maxRetryDelayMs = config.delivery.maxRetryDelayMs;
524
1159
  if (config.delivery?.maxPayloadBytes !== void 0) deliveryConfig.maxPayloadBytes = config.delivery.maxPayloadBytes;
1160
+ if (config.delivery?.payloadSizePolicy !== void 0) deliveryConfig.payloadSizePolicy = config.delivery.payloadSizePolicy;
1161
+ if (config.delivery?.payloadTruncateStringLength !== void 0) {
1162
+ deliveryConfig.payloadTruncateStringLength = config.delivery.payloadTruncateStringLength;
1163
+ }
1164
+ if (config.delivery?.useBeacon !== void 0) deliveryConfig.useBeacon = config.delivery.useBeacon;
525
1165
  if (config.delivery?.batchSize !== void 0) deliveryConfig.batchSize = config.delivery.batchSize;
526
1166
  if (config.onDrop !== void 0) deliveryConfig.onDrop = config.onDrop;
527
1167
  if (config.onError !== void 0) deliveryConfig.onError = config.onError;
528
- this.delivery = createDeliveryManager(storage ? {
529
- ...deliveryConfig,
530
- storage
531
- } : deliveryConfig);
1168
+ this.delivery = createDeliveryManager(deliveryConfig);
532
1169
  this.initialRetryDelayMs = config.delivery?.retryDelayMs ?? 1e3;
533
1170
  this.retryDelayMs = this.initialRetryDelayMs;
534
1171
  this.maxRetryDelayMs = config.delivery?.maxRetryDelayMs ?? 3e4;
535
1172
  this.writeKey = config.writeKey || config.token || "";
536
- this.identity = { anonymousId: createId2("anon") };
537
- this.sessionId = createId2("session");
1173
+ this.identity = { anonymousId: resolveAnonymousId(this.persistence) };
1174
+ captureAttributionFromUrl(this.persistence);
538
1175
  this.consentState = normalizeConsent(config.gdprConsent ?? config.consent);
539
1176
  this.gdprConsent = this.consentState.analytics;
1177
+ this.installAutocapture();
540
1178
  if (!config.delivery?.disableLifecycleFlush) {
541
1179
  this.installLifecycleFlush();
542
1180
  }
@@ -576,7 +1214,8 @@ var DriveMetaDataSDK = class {
576
1214
  }
577
1215
  }
578
1216
  reset() {
579
- this.identity = { anonymousId: createId2("anon") };
1217
+ this.identity = { anonymousId: resetAnonymousId(this.persistence) };
1218
+ this.session.reset();
580
1219
  this.queue = [];
581
1220
  this.offline = false;
582
1221
  if (this.retryTimer !== void 0) {
@@ -585,8 +1224,20 @@ var DriveMetaDataSDK = class {
585
1224
  }
586
1225
  this.lifecycleCleanup?.();
587
1226
  this.lifecycleCleanup = void 0;
1227
+ this.autocaptureCleanup?.();
1228
+ this.autocaptureCleanup = void 0;
588
1229
  this.delivery.clearQueue("manual_clear");
589
1230
  }
1231
+ disposeForTests() {
1232
+ if (this.retryTimer !== void 0) {
1233
+ clearTimeout(this.retryTimer);
1234
+ this.retryTimer = void 0;
1235
+ }
1236
+ this.lifecycleCleanup?.();
1237
+ this.lifecycleCleanup = void 0;
1238
+ this.autocaptureCleanup?.();
1239
+ this.autocaptureCleanup = void 0;
1240
+ }
590
1241
  setConsent(consent) {
591
1242
  this.consentState = typeof consent === "object" ? mergeConsent(this.consentState, consent) : normalizeConsent(consent);
592
1243
  this.gdprConsent = this.consentState.analytics;
@@ -601,6 +1252,7 @@ var DriveMetaDataSDK = class {
601
1252
  initialized: this.initialized,
602
1253
  consent: this.gdprConsent,
603
1254
  consentPurposes: this.consentState,
1255
+ persistence: this.persistence.getHealth(),
604
1256
  queueSize: deliveryDiagnostics.queued,
605
1257
  offline: this.offline || deliveryDiagnostics.queued > 0,
606
1258
  droppedEvents: this.droppedEvents + deliveryDiagnostics.dropped.length,
@@ -615,7 +1267,9 @@ var DriveMetaDataSDK = class {
615
1267
  void this.delivery.send(payload).then(() => {
616
1268
  const diagnostics = this.delivery.getDiagnostics();
617
1269
  this.offline = diagnostics.queued > 0;
618
- this.lastError = diagnostics.lastError;
1270
+ if (diagnostics.lastError !== void 0) {
1271
+ this.lastError = diagnostics.lastError;
1272
+ }
619
1273
  if (diagnostics.queued > 0) {
620
1274
  this.scheduleRetryFlush();
621
1275
  }
@@ -647,6 +1301,28 @@ var DriveMetaDataSDK = class {
647
1301
  }
648
1302
  };
649
1303
  }
1304
+ installAutocapture() {
1305
+ const browserWindow = getBrowserWindow();
1306
+ if (!browserWindow) return;
1307
+ if (this.config.autocapture === false) return;
1308
+ const capturePageview = this.config.capturePageview !== false;
1309
+ const capturePageleave = this.config.capturePageleave !== false;
1310
+ const controller = installAutocapture({
1311
+ browserWindow,
1312
+ capturePageview,
1313
+ capturePageleave,
1314
+ onPageView: () => {
1315
+ this.page();
1316
+ },
1317
+ onPageLeave: () => {
1318
+ this.trackEvent("page_leave", { url: browserWindow.location.href });
1319
+ },
1320
+ onRouteChange: () => {
1321
+ captureAttributionFromUrl(this.persistence);
1322
+ }
1323
+ });
1324
+ this.autocaptureCleanup = controller.cleanup;
1325
+ }
650
1326
  scheduleRetryFlush() {
651
1327
  if (this.retryTimer !== void 0) return;
652
1328
  const delay = Math.min(this.retryDelayMs, this.maxRetryDelayMs);
@@ -672,7 +1348,7 @@ var DriveMetaDataSDK = class {
672
1348
  type,
673
1349
  event,
674
1350
  properties: sanitizeProperties(properties),
675
- messageId: options.messageId ?? createId2("msg"),
1351
+ messageId: ensureUuid(options.messageId),
676
1352
  timestamp: options.timestamp ?? (/* @__PURE__ */ new Date()).toISOString(),
677
1353
  context: options.context ?? {},
678
1354
  anonymousId: this.identity.anonymousId,
@@ -681,7 +1357,7 @@ var DriveMetaDataSDK = class {
681
1357
  appId: this.config.appId,
682
1358
  writeKey: this.writeKey,
683
1359
  consent: this.consentState,
684
- sessionId: this.sessionId
1360
+ sessionId: this.session.getSessionId()
685
1361
  };
686
1362
  if (this.identity.userId !== void 0) prepared.userId = this.identity.userId;
687
1363
  if (this.identity.groupId !== void 0) prepared.groupId = this.identity.groupId;
@@ -705,7 +1381,17 @@ var DriveMetaDataSDK = class {
705
1381
  if (!validation.ok) {
706
1382
  this.lastError = `DMD SDK schema warning: ${validation.errors.join(", ")}`;
707
1383
  }
708
- this.sendEvent(this.toCollectorPayload(prepared));
1384
+ const collectorPayload = this.toCollectorPayload(prepared);
1385
+ const backendValidation = validateBackendCollectorPayload(collectorPayload);
1386
+ if (!backendValidation.ok && this.config.schemaValidation === "strict") {
1387
+ this.lastError = `DMD SDK backend schema warning: ${backendValidation.errors.join(", ")}`;
1388
+ this.recordDrop(type, "invalid_payload", event);
1389
+ return;
1390
+ }
1391
+ if (!backendValidation.ok) {
1392
+ this.lastError = `DMD SDK backend schema warning: ${backendValidation.errors.join(", ")}`;
1393
+ }
1394
+ this.sendEvent(collectorPayload);
709
1395
  }
710
1396
  toCollectorPayload(prepared) {
711
1397
  const browserWindow = getBrowserWindow();
@@ -721,14 +1407,14 @@ var DriveMetaDataSDK = class {
721
1407
  ua: browserWindow?.navigator?.userAgent,
722
1408
  appId: prepared.appId,
723
1409
  pageUrl: browserWindow?.location?.href,
724
- eventData: {
1410
+ eventData: mergeStoredAttribution({
725
1411
  ...prepared.properties,
726
1412
  anonymousId: prepared.anonymousId,
727
1413
  sessionId: prepared.sessionId,
728
1414
  timestamp: prepared.timestamp,
729
1415
  requestSentAt: prepared.timestamp,
730
1416
  requestReceivedAt: prepared.timestamp
731
- }
1417
+ }, this.persistence)
732
1418
  });
733
1419
  }
734
1420
  recordDrop(type, reason, event) {
@@ -804,7 +1490,7 @@ function initDmdSDK(config) {
804
1490
  return publicSingleton;
805
1491
  }
806
1492
  try {
807
- const instance = new DriveMetaDataSDK(config);
1493
+ const instance = new DriveMetaDataSDK(normalizeBrowserConfig(config));
808
1494
  return setSingleton(instance);
809
1495
  } catch (error) {
810
1496
  lastError = error instanceof Error ? error.message : String(error);