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