saasco-sdk 0.2.4 → 0.2.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -38,7 +38,187 @@ module.exports = __toCommonJS(index_exports);
38
38
  var import_psl = require("psl");
39
39
 
40
40
  // package.json
41
- var version = "0.2.4";
41
+ var version = "0.2.6";
42
+
43
+ // src/lib/deliverOnce.ts
44
+ var sent = /* @__PURE__ */ new Set();
45
+ var pending = /* @__PURE__ */ new Map();
46
+ function deliverOnce(key, send) {
47
+ if (!key || typeof window === "undefined") {
48
+ return Promise.resolve().then(send);
49
+ }
50
+ if (sent.has(key)) {
51
+ return Promise.resolve();
52
+ }
53
+ try {
54
+ if (window.sessionStorage.getItem(key)) {
55
+ return Promise.resolve();
56
+ }
57
+ } catch {
58
+ }
59
+ const existing = pending.get(key);
60
+ if (existing) {
61
+ return existing;
62
+ }
63
+ const task = Promise.resolve().then(send).then(() => {
64
+ sent.add(key);
65
+ try {
66
+ window.sessionStorage.setItem(key, "true");
67
+ } catch {
68
+ }
69
+ }).finally(() => pending.delete(key));
70
+ pending.set(key, task);
71
+ return task;
72
+ }
73
+
74
+ // ../shared/src/lib/analyticsIngestError.ts
75
+ var import_zod = require("zod");
76
+ var IDENTIFY_INGEST_ERROR_SOURCE = "saasco-identify";
77
+ var ANALYTICS_INGEST_ERROR_SOURCE = "saasco-analytics";
78
+ var AnalyticsIngestErrorCodeSchema = import_zod.z.enum([
79
+ "CREDENTIAL_INVALID",
80
+ "RESOURCE_FORBIDDEN",
81
+ "RESOURCE_UNSPECIFIED",
82
+ "ENVIRONMENT_UNREACHABLE",
83
+ "INGEST_FAILED"
84
+ ]);
85
+ var AnalyticsIngestSurfaceSchema = import_zod.z.enum(["identify", "events"]);
86
+ var AnalyticsIngestErrorSchema = import_zod.z.object({
87
+ affected: import_zod.z.string(),
88
+ code: AnalyticsIngestErrorCodeSchema,
89
+ message: import_zod.z.string(),
90
+ projectId: import_zod.z.string().nullable(),
91
+ recovery: import_zod.z.string(),
92
+ resource: import_zod.z.string().nullable(),
93
+ surface: AnalyticsIngestSurfaceSchema
94
+ });
95
+ var IDENTIFY_AFFECTED = "Saasco identification only: CRM contact properties and identity stitching. Page views, charts, and the host application UI are not this failure.";
96
+ var ANALYTICS_EVENT_AFFECTED = "Saasco event tracking only. The host application UI is not this failure.";
97
+ function analyticsIngestError({
98
+ code,
99
+ message,
100
+ projectId,
101
+ resource,
102
+ surface
103
+ }) {
104
+ const copy = ingestCopy({ code, resource, surface });
105
+ return {
106
+ affected: surface === "identify" ? IDENTIFY_AFFECTED : ANALYTICS_EVENT_AFFECTED,
107
+ code,
108
+ message: message ?? copy.message,
109
+ projectId,
110
+ recovery: copy.recovery,
111
+ resource,
112
+ surface
113
+ };
114
+ }
115
+ function ingestCopy({
116
+ code,
117
+ resource,
118
+ surface
119
+ }) {
120
+ const named = resource ? `\`${resource}\`` : "the requested analytics resource";
121
+ const didNotComplete = surface === "identify" ? "Identification did not complete." : "Event tracking did not complete.";
122
+ switch (code) {
123
+ case "CREDENTIAL_INVALID": {
124
+ return {
125
+ message: "Saasco analytics credentials are invalid or missing. This is not an application login or permissions problem.",
126
+ recovery: "Check TINYBIRD_ANALYTICS_TOKEN on the Saasco ingest worker. Do not change host-app auth. Charts and UI styling are unrelated."
127
+ };
128
+ }
129
+ case "RESOURCE_FORBIDDEN": {
130
+ return {
131
+ message: `Saasco analytics is not allowed to use ${named}. ${didNotComplete}`,
132
+ recovery: surface === "identify" ? `Grant the ingest worker access to ${named} (PIPES:READ or DATASOURCES:APPEND). Page tracking may still work.` : `Grant the ingest worker access to ${named} (PIPES:READ or DATASOURCES:APPEND).`
133
+ };
134
+ }
135
+ case "RESOURCE_UNSPECIFIED": {
136
+ return {
137
+ message: `Saasco analytics was denied access without a named pipe or datasource. ${didNotComplete}`,
138
+ recovery: "This is a Saasco ingest configuration issue: the worker could not name the Tinybird resource it tried to use. Host-app login is unrelated."
139
+ };
140
+ }
141
+ case "ENVIRONMENT_UNREACHABLE": {
142
+ return {
143
+ message: surface === "identify" ? "The Saasco ingest host could not be reached, so identification was not sent." : "The Saasco ingest host could not be reached, so this event was not sent.",
144
+ recovery: "If `proxy` points at localhost, start the analytics worker (`pnpm worker:dev`) or use https://t.saasco.com/. This does not mean the host app is broken."
145
+ };
146
+ }
147
+ default: {
148
+ return {
149
+ message: surface === "identify" ? "Saasco identification could not be completed." : "Saasco event tracking could not be completed.",
150
+ recovery: "Retry. If it keeps failing, this is a Saasco analytics ingest issue, not host-app login or chart configuration."
151
+ };
152
+ }
153
+ }
154
+ }
155
+ function ingestErrorSource(surface) {
156
+ return surface === "identify" ? IDENTIFY_INGEST_ERROR_SOURCE : ANALYTICS_INGEST_ERROR_SOURCE;
157
+ }
158
+ function redactSecrets(text) {
159
+ return text.replaceAll(/Bearer\s+\S+/giu, "Bearer [redacted]").replaceAll(/\bp\.[A-Za-z0-9._-]+/gu, "[redacted]").replaceAll(/token=[^&\s]+/giu, "token=[redacted]");
160
+ }
161
+ function isUnsafeIngestMessage(text) {
162
+ return /Bearer \[redacted\]|token=\[redacted\]/iu.test(text) || /token you have provided|doesn't have access to this resource|not enough permissions|invalid token/iu.test(
163
+ text
164
+ );
165
+ }
166
+
167
+ // src/lib/formatAnalyticsRequestFailure.ts
168
+ function formatAnalyticsRequestFailure(input) {
169
+ const surface = input.path === "identify" ? "identify" : "events";
170
+ const projectId = input.projectId ?? null;
171
+ if (input.reason === "http") {
172
+ const parsed = AnalyticsIngestErrorSchema.safeParse(input.body);
173
+ if (parsed.success) {
174
+ return parsed.data;
175
+ }
176
+ }
177
+ if (input.reason === "unreachable") {
178
+ return analyticsIngestError({
179
+ code: "ENVIRONMENT_UNREACHABLE",
180
+ projectId,
181
+ resource: null,
182
+ surface
183
+ });
184
+ }
185
+ const legacy = input.body && typeof input.body === "object" && !Array.isArray(input.body) && "message" in input.body && typeof input.body.message === "string" ? input.body.message : "";
186
+ const safeMessage = redactSecrets(legacy).trim();
187
+ return analyticsIngestError({
188
+ code: "INGEST_FAILED",
189
+ message: safeMessage && !isUnsafeIngestMessage(safeMessage) ? safeMessage : void 0,
190
+ projectId,
191
+ resource: null,
192
+ surface
193
+ });
194
+ }
195
+ function reportIngestFailure({
196
+ failure,
197
+ logger,
198
+ path,
199
+ seen
200
+ }) {
201
+ const key = `${path}:${failure.code}:${failure.resource ?? ""}`;
202
+ if (!seen.has(key)) {
203
+ seen.add(key);
204
+ logger.warn(...analyticsFailureLogLines(failure, path));
205
+ }
206
+ return {
207
+ success: false,
208
+ message: failure.message
209
+ };
210
+ }
211
+ function analyticsFailureLogLines(failure, path) {
212
+ const lines = [
213
+ `${ingestErrorSource(failure.surface)} ${path} failed (${failure.code})`,
214
+ failure.message,
215
+ failure.resource ? `Resource: ${failure.resource}` : null,
216
+ failure.projectId ? `Project: ${failure.projectId}` : null,
217
+ `Affected: ${failure.affected}`,
218
+ `Recovery: ${failure.recovery}`
219
+ ];
220
+ return lines.filter((line) => line !== null);
221
+ }
42
222
 
43
223
  // src/lib/timezones.ts
44
224
  var timezones = {
@@ -487,6 +667,9 @@ var AnalyticsLogger = class {
487
667
  constructor(config) {
488
668
  this.config = config;
489
669
  }
670
+ setLevel(level) {
671
+ this.config.level = level;
672
+ }
490
673
  debug(...args) {
491
674
  if (this.config.level < 3 /* DEBUG */) {
492
675
  return;
@@ -515,12 +698,10 @@ var AnalyticsLogger = class {
515
698
  );
516
699
  }
517
700
  /**
518
- * Log warning information
701
+ * Warnings always print. Ingest failures use this so they stay visible when
702
+ * the default log level is ERROR, without `console.error` (Next.js overlays it).
519
703
  */
520
704
  warn(...args) {
521
- if (this.config.level < 1 /* WARN */) {
522
- return;
523
- }
524
705
  const icon = "\u25CD";
525
706
  const message = `${icon} ${this.config.label}`;
526
707
  console.warn(
@@ -567,9 +748,34 @@ function getIntegrationLoggerLevel(integrationName, debug) {
567
748
  }
568
749
 
569
750
  // src/lib/utils/uuid.ts
570
- var import_uuid = require("@lukeed/uuid");
751
+ var HEX = [];
752
+ for (let i = 0; i < 256; i++) {
753
+ HEX[i] = (i + 256).toString(16).slice(1);
754
+ }
571
755
  function uuid() {
572
- return (0, import_uuid.v4)();
756
+ const bytes = randomBytes(16);
757
+ bytes[6] = bytes[6] & 15 | 64;
758
+ bytes[8] = bytes[8] & 63 | 128;
759
+ let out = "";
760
+ for (let i = 0; i < 16; i++) {
761
+ out += HEX[bytes[i]];
762
+ if (i === 3 || i === 5 || i === 7 || i === 9) {
763
+ out += "-";
764
+ }
765
+ }
766
+ return out;
767
+ }
768
+ function randomBytes(length) {
769
+ const bytes = new Uint8Array(length);
770
+ const webCrypto = globalThis.crypto;
771
+ if (typeof webCrypto?.getRandomValues === "function") {
772
+ webCrypto.getRandomValues(bytes);
773
+ return bytes;
774
+ }
775
+ for (let i = 0; i < length; i++) {
776
+ bytes[i] = Math.random() * 256 | 0;
777
+ }
778
+ return bytes;
573
779
  }
574
780
 
575
781
  // src/lib/getBrowserContext.ts
@@ -641,6 +847,50 @@ function resolveAdParams(params) {
641
847
 
642
848
  // src/lib/integrations/facebook-pixel.ts
643
849
  var import_js_sha256 = require("js-sha256");
850
+
851
+ // src/lib/integrations/loadPixelScript.ts
852
+ var loads = /* @__PURE__ */ new Map();
853
+ function loadPixelScript(src) {
854
+ const existingLoad = loads.get(src);
855
+ if (existingLoad) {
856
+ return existingLoad;
857
+ }
858
+ const existingScript = Array.from(document.scripts).find(
859
+ (script2) => script2.src === src
860
+ );
861
+ const script = existingScript ?? document.createElement("script");
862
+ const task = new Promise((resolve, reject) => {
863
+ const cleanup = () => {
864
+ clearTimeout(timeout);
865
+ script.removeEventListener("load", loaded);
866
+ script.removeEventListener("error", failed);
867
+ };
868
+ const loaded = () => {
869
+ cleanup();
870
+ resolve();
871
+ };
872
+ const failed = () => {
873
+ cleanup();
874
+ script.remove();
875
+ reject(new Error(`Pixel script unavailable: ${src}`));
876
+ };
877
+ const timeout = setTimeout(failed, 1e4);
878
+ script.addEventListener("load", loaded);
879
+ script.addEventListener("error", failed);
880
+ if (!existingScript) {
881
+ script.async = true;
882
+ script.src = src;
883
+ document.head.append(script);
884
+ }
885
+ }).catch((error) => {
886
+ loads.delete(src);
887
+ throw error;
888
+ });
889
+ loads.set(src, task);
890
+ return task;
891
+ }
892
+
893
+ // src/lib/integrations/facebook-pixel.ts
644
894
  var isBrowser2 = typeof window !== "undefined";
645
895
  var isServer2 = !isBrowser2;
646
896
  var standardFacebookEvents = [
@@ -682,70 +932,32 @@ function createFacebookPixelIntegration(config, debug) {
682
932
  return;
683
933
  }
684
934
  logger.info("Initializing Facebook Pixel", { pixelId: config.pixelId });
685
- if (window.fbq || window._fbq) {
686
- logger.warn("Facebook Pixel is already initialized");
687
- isPixelReady = true;
688
- return;
689
- }
690
- return new Promise((resolve, reject) => {
691
- try {
692
- (function init(f, b, e, v, n, t, s) {
693
- if (f.fbq) {
694
- return;
695
- }
696
- n = f.fbq = function fbq2() {
697
- n.callMethod ? n.callMethod.apply(n, arguments) : n.queue.push(arguments);
698
- };
699
- if (!f._fbq) {
700
- f._fbq = n;
701
- }
702
- n.push = n;
703
- n.loaded = true;
704
- n.version = "2.0";
705
- n.queue = [];
706
- t = b.createElement(e);
707
- t.async = true;
708
- t.src = v;
709
- t.addEventListener("load", () => {
710
- isPixelReady = true;
711
- logger.info("Facebook Pixel script loaded successfully");
712
- resolve();
713
- });
714
- t.addEventListener("error", () => {
715
- logger.error("Failed to load Facebook Pixel script");
716
- reject(new Error("Failed to load Facebook Pixel script"));
717
- });
718
- s = b.getElementsByTagName(e)[0];
719
- s.parentNode.insertBefore(t, s);
720
- })(
721
- window,
722
- document,
723
- "script",
724
- "https://connect.facebook.net/en_US/fbevents.js"
725
- );
726
- const { fbq } = window;
727
- if (!fbq) {
728
- reject(new Error("Facebook Pixel failed to initialize"));
729
- return;
730
- }
731
- fbq.disablePushState = true;
732
- fbq.allowDuplicatePageViews = true;
733
- if (config.automaticConfiguration === false) {
734
- logger.debug("Disabling automatic configuration");
735
- fbq("set", "autoConfig", false, config.pixelId);
736
- }
737
- logger.debug("Initializing Facebook Pixel with ID", config.pixelId);
738
- fbq("init", config.pixelId);
739
- if (typeof fbq === "function") {
740
- isPixelReady = true;
741
- logger.info("Facebook Pixel initialized synchronously");
742
- resolve();
935
+ if (!window.fbq) {
936
+ const fbq2 = (...args) => {
937
+ if (fbq2.callMethod) {
938
+ fbq2.callMethod(...args);
939
+ } else {
940
+ fbq2.queue.push(args);
743
941
  }
744
- } catch (error) {
745
- logger.error("Error during Facebook Pixel initialization", error);
746
- reject(error);
747
- }
748
- });
942
+ };
943
+ fbq2.queue = [];
944
+ fbq2.push = fbq2;
945
+ fbq2.loaded = true;
946
+ fbq2.version = "2.0";
947
+ window.fbq = fbq2;
948
+ window._fbq = fbq2;
949
+ }
950
+ const { fbq } = window;
951
+ fbq.disablePushState = true;
952
+ fbq.allowDuplicatePageViews = true;
953
+ if (config.automaticConfiguration === false) {
954
+ fbq("set", "autoConfig", false, config.pixelId);
955
+ }
956
+ fbq("init", config.pixelId);
957
+ if (!fbq.callMethod) {
958
+ await loadPixelScript("https://connect.facebook.net/en_US/fbevents.js");
959
+ }
960
+ isPixelReady = true;
749
961
  },
750
962
  track: (eventName, properties, _context) => {
751
963
  const { fbq } = window;
@@ -753,9 +965,11 @@ function createFacebookPixelIntegration(config, debug) {
753
965
  logger.warn("Facebook Pixel not ready, skipping track event", {
754
966
  eventName
755
967
  });
756
- return;
968
+ throw new Error("Facebook Pixel not ready");
757
969
  }
758
970
  const fbEventName = getFacebookEventName(eventName, config.eventMapping);
971
+ const eventId = properties?.["event_id"];
972
+ const eventOptions = typeof eventId === "string" && eventId.trim().length > 0 ? { eventID: eventId } : void 0;
759
973
  const eventsWithParams = ["Purchase", "StartTrial", "Subscribe"];
760
974
  const isStandardEvent = standardFacebookEvents.includes(
761
975
  fbEventName
@@ -783,10 +997,18 @@ function createFacebookPixelIntegration(config, debug) {
783
997
  event: fbEventName,
784
998
  params: fbParams
785
999
  });
786
- fbq(trackType, fbEventName, fbParams);
1000
+ if (eventOptions) {
1001
+ fbq(trackType, fbEventName, fbParams, eventOptions);
1002
+ } else {
1003
+ fbq(trackType, fbEventName, fbParams);
1004
+ }
787
1005
  } else {
788
1006
  logger.info("Tracking Facebook Pixel event", { event: fbEventName });
789
- fbq(trackType, fbEventName);
1007
+ if (eventOptions) {
1008
+ fbq(trackType, fbEventName, {}, eventOptions);
1009
+ } else {
1010
+ fbq(trackType, fbEventName);
1011
+ }
790
1012
  }
791
1013
  },
792
1014
  identify: (userId, properties, _context) => {
@@ -1140,12 +1362,12 @@ var IntegrationManager = class {
1140
1362
  globalQueue = [];
1141
1363
  config;
1142
1364
  logger;
1143
- initTime;
1144
1365
  currentEnvironment;
1145
1366
  flushTimer;
1146
1367
  unloadHandler;
1147
1368
  constructor(config = {}) {
1148
1369
  this.config = {
1370
+ deduplicationNamespace: config.deduplicationNamespace ?? "default",
1149
1371
  flushInterval: config.flushInterval ?? 5e3,
1150
1372
  loggerLevel: config.loggerLevel ?? 0 /* ERROR */,
1151
1373
  maxIntegrationWaitTime: config.maxIntegrationWaitTime ?? 1e4,
@@ -1155,7 +1377,6 @@ var IntegrationManager = class {
1155
1377
  label: "Saasco Integrations Debug",
1156
1378
  level: this.config.loggerLevel
1157
1379
  });
1158
- this.initTime = Date.now();
1159
1380
  this.currentEnvironment = typeof window === "undefined" ? "server" : "client";
1160
1381
  this.setupPeriodicFlushing();
1161
1382
  this.setupUnloadHandler();
@@ -1232,6 +1453,8 @@ var IntegrationManager = class {
1232
1453
  */
1233
1454
  send(partial) {
1234
1455
  const envelope = {
1456
+ delivered: /* @__PURE__ */ new Set(),
1457
+ pending: /* @__PURE__ */ new Set(),
1235
1458
  context: { ...this.context },
1236
1459
  id: uuid(),
1237
1460
  name: partial.name,
@@ -1239,65 +1462,55 @@ var IntegrationManager = class {
1239
1462
  timestamp: Date.now(),
1240
1463
  type: partial.type
1241
1464
  };
1242
- if (!this.isReady()) {
1243
- if (this.globalQueue.length >= this.config.maxQueueSize) {
1244
- this.globalQueue.shift();
1245
- this.logger.warn("Queue full \u2192 dropped oldest");
1246
- }
1247
- this.globalQueue.push(envelope);
1248
- this.logger.debug(`Queued (${this.globalQueue.length})`, envelope);
1249
- return;
1465
+ if (this.globalQueue.length >= this.config.maxQueueSize) {
1466
+ this.globalQueue.shift();
1467
+ this.logger.warn("Queue full \u2192 dropped oldest");
1250
1468
  }
1251
- this.deliver(envelope);
1469
+ this.globalQueue.push(envelope);
1470
+ this.flush();
1252
1471
  }
1253
- async deliver(evt) {
1254
- for (const { integration, status } of this.integrations.values()) {
1255
- if (status !== "ready") {
1256
- continue;
1257
- }
1258
- try {
1259
- if (evt.type === "track" && integration.track && evt.name) {
1260
- this.logger.debug(`Track (${integration.name}) ${evt.name}`, {
1261
- context: evt.context,
1262
- properties: evt.properties
1263
- });
1264
- await integration.track(evt.name, evt.properties, evt.context);
1265
- } else if (evt.type === "identify" && integration.identify) {
1266
- this.logger.debug(`Identify (${integration.name})`, {
1267
- context: evt.context,
1268
- properties: evt.properties,
1269
- userId: evt.context.distinctId
1270
- });
1271
- await integration.identify(
1272
- evt.context.distinctId,
1273
- evt.properties,
1274
- evt.context
1275
- );
1276
- }
1277
- } catch (error) {
1278
- this.logger.error(
1279
- `deliver error in ${integration.name} (${evt.type})`,
1280
- error
1472
+ async deliver(evt, state) {
1473
+ const { integration } = state;
1474
+ evt.pending.add(integration.name);
1475
+ try {
1476
+ if (evt.type === "track" && integration.track && evt.name) {
1477
+ const eventId = evt.properties?.["event_id"];
1478
+ const key = typeof eventId === "string" && eventId.trim() ? `saasco:${this.config.deduplicationNamespace}:${integration.name}:${evt.name}:${eventId}` : void 0;
1479
+ const { track } = integration;
1480
+ const { name } = evt;
1481
+ await deliverOnce(
1482
+ key,
1483
+ () => track.call(integration, name, evt.properties, evt.context)
1484
+ );
1485
+ } else if (evt.type === "identify" && integration.identify) {
1486
+ await integration.identify(
1487
+ evt.context.distinctId,
1488
+ evt.properties,
1489
+ evt.context
1281
1490
  );
1282
1491
  }
1492
+ evt.delivered.add(integration.name);
1493
+ } catch (error) {
1494
+ this.logger.error(
1495
+ `deliver error in ${integration.name} (${evt.type})`,
1496
+ error
1497
+ );
1498
+ } finally {
1499
+ evt.pending.delete(integration.name);
1283
1500
  }
1284
1501
  }
1285
- /**
1286
- * Flush queued events FIFO once at least one integration is ready.
1287
- */
1502
+ /** Each ready destination drains its own copy; a slow/blocked pixel cannot
1503
+ * discard another pixel's queued events or hold up the ready destinations. */
1288
1504
  flush() {
1289
- if (this.globalQueue.length === 0) {
1290
- return;
1291
- }
1292
- if (!this.isReady()) {
1293
- return;
1294
- }
1295
- this.logger.debug(`flushing ${this.globalQueue.length} queued events`);
1296
- const toSend = this.globalQueue;
1297
- this.globalQueue = [];
1298
- for (const evt of toSend) {
1299
- this.deliver(evt);
1300
- }
1505
+ this.globalQueue = this.globalQueue.filter((evt) => {
1506
+ for (const state of this.integrations.values()) {
1507
+ const { name } = state.integration;
1508
+ if (state.status === "ready" && !evt.delivered.has(name) && !evt.pending.has(name)) {
1509
+ void this.deliver(evt, state);
1510
+ }
1511
+ }
1512
+ return evt.delivered.size < this.integrations.size && Date.now() - evt.timestamp < this.config.maxIntegrationWaitTime;
1513
+ });
1301
1514
  }
1302
1515
  readyCount() {
1303
1516
  let n = 0;
@@ -1308,18 +1521,6 @@ var IntegrationManager = class {
1308
1521
  }
1309
1522
  return n;
1310
1523
  }
1311
- isReady() {
1312
- const timeElapsed = Date.now() - this.initTime;
1313
- if (this.readyCount() < this.integrations.size && timeElapsed < this.config.maxIntegrationWaitTime) {
1314
- return false;
1315
- }
1316
- if (this.readyCount() < this.integrations.size) {
1317
- this.logger.info(
1318
- `proceeding with ${this.readyCount()}/${this.integrations.size} integrations ready after ${timeElapsed}ms wait`
1319
- );
1320
- }
1321
- return true;
1322
- }
1323
1524
  /**
1324
1525
  * Setup periodic flushing if enabled
1325
1526
  */
@@ -1403,50 +1604,20 @@ function createPinterestTagIntegration(config, debug) {
1403
1604
  return;
1404
1605
  }
1405
1606
  logger.info("Initializing Pinterest Tag", { tagId: config.tagId });
1406
- if (window.pintrk || window._pintrk) {
1407
- logger.warn("Pinterest Tag is already initialized");
1408
- isTagReady = true;
1409
- return;
1607
+ if (!window.pintrk) {
1608
+ const pintrk = (...args) => pintrk.queue.push(args);
1609
+ pintrk.queue = [];
1610
+ pintrk.version = "3.0";
1611
+ window.pintrk = pintrk;
1410
1612
  }
1411
- return new Promise((resolve, reject) => {
1412
- try {
1413
- (function init(e) {
1414
- if (!window.pintrk) {
1415
- window.pintrk = function pintrk(...args) {
1416
- window.pintrk.queue.push(args);
1417
- };
1418
- const n = window.pintrk;
1419
- n.queue = [];
1420
- n.version = "3.0";
1421
- const t = document.createElement("script");
1422
- t.async = true;
1423
- t.src = e;
1424
- t.addEventListener("load", () => {
1425
- window.pintrk("load", config.tagId);
1426
- window.pintrk("page");
1427
- setTimeout(() => {
1428
- logger.debug("Pinterest Tag initialized");
1429
- isTagReady = true;
1430
- resolve();
1431
- }, 500);
1432
- });
1433
- t.addEventListener("error", () => {
1434
- logger.error("Failed to load Pinterest Tag script");
1435
- reject(new Error("Failed to load Pinterest Tag script"));
1436
- });
1437
- const r = document.getElementsByTagName("script")[0];
1438
- r.parentNode.insertBefore(t, r);
1439
- }
1440
- })("https://s.pinimg.com/ct/core.js");
1441
- } catch (error) {
1442
- logger.error("Failed to initialize Pinterest Tag", error);
1443
- reject(error);
1444
- }
1445
- });
1613
+ await loadPixelScript("https://s.pinimg.com/ct/core.js");
1614
+ window.pintrk("load", config.tagId);
1615
+ window.pintrk("page");
1616
+ isTagReady = true;
1446
1617
  },
1447
1618
  track: (eventName, properties) => {
1448
1619
  if (!isTagReady || !window.pintrk) {
1449
- return;
1620
+ throw new Error("Pinterest Tag not ready");
1450
1621
  }
1451
1622
  const pinterestEventName = getPinterestEventName(
1452
1623
  eventName,
@@ -1575,8 +1746,8 @@ function createTikTokPixelIntegration(config, debug) {
1575
1746
  return;
1576
1747
  }
1577
1748
  logger.info("Initializing TikTok Pixel", { pixelId: config.pixelId });
1578
- if (window.ttq) {
1579
- logger.warn("TikTok Pixel is already initialized");
1749
+ if (window.ttq?.ready) {
1750
+ await new Promise((resolve) => window.ttq.ready(resolve));
1580
1751
  isPixelReady = true;
1581
1752
  return;
1582
1753
  }
@@ -1628,28 +1799,15 @@ function createTikTokPixelIntegration(config, debug) {
1628
1799
  ttq._t[e] = Date.now();
1629
1800
  ttq._o ||= {};
1630
1801
  ttq._o[e] = n || {};
1631
- const script = document.createElement("script");
1632
- script.type = "text/javascript";
1633
- script.async = true;
1634
- script.src = `${r}?sdkid=${e}&lib=${t}`;
1635
- script.addEventListener("load", () => {
1802
+ loadPixelScript(`${r}?sdkid=${e}&lib=${t}`).then(() => {
1636
1803
  isPixelReady = true;
1637
1804
  resolve();
1638
- });
1639
- script.addEventListener("error", () => {
1640
- reject(new Error("Failed to load TikTok Pixel script"));
1641
- });
1642
- const firstScript = document.getElementsByTagName("script")[0];
1643
- firstScript.parentNode.insertBefore(script, firstScript);
1805
+ }).catch(reject);
1644
1806
  };
1645
1807
  ttq.load(
1646
1808
  config.pixelId,
1647
1809
  config.testMode ? { test_mode: true } : void 0
1648
1810
  );
1649
- if (window.ttq && typeof window.ttq === "function") {
1650
- isPixelReady = true;
1651
- resolve();
1652
- }
1653
1811
  })(window, document, "ttq");
1654
1812
  } catch (error) {
1655
1813
  reject(error);
@@ -1658,8 +1816,8 @@ function createTikTokPixelIntegration(config, debug) {
1658
1816
  },
1659
1817
  track: (eventName, properties, _context) => {
1660
1818
  const { ttq } = window;
1661
- if (!isPixelReady || !ttq) {
1662
- return;
1819
+ if (!isPixelReady || !ttq?.track) {
1820
+ throw new Error("TikTok Pixel not ready");
1663
1821
  }
1664
1822
  const ttEventName = getTikTokEventName(eventName, config.eventMapping);
1665
1823
  if (ttEventName === "PageView") {
@@ -1667,7 +1825,10 @@ function createTikTokPixelIntegration(config, debug) {
1667
1825
  return;
1668
1826
  }
1669
1827
  const ttParams = mapTikTokEventParameters(ttEventName, properties);
1670
- if (ttParams && Object.keys(ttParams).length > 0) {
1828
+ const eventId = properties?.["event_id"];
1829
+ if (typeof eventId === "string" && eventId.trim().length > 0) {
1830
+ ttq.track?.(ttEventName, ttParams ?? {}, { event_id: eventId });
1831
+ } else if (ttParams && Object.keys(ttParams).length > 0) {
1671
1832
  ttq.track?.(ttEventName, ttParams);
1672
1833
  } else {
1673
1834
  ttq.track?.(ttEventName);
@@ -1875,6 +2036,41 @@ function normalizeSaascoUrl(url) {
1875
2036
  return parsed.toString();
1876
2037
  }
1877
2038
 
2039
+ // src/lib/widgetLoader.ts
2040
+ function isEnabledPayload(data2) {
2041
+ return typeof data2 === "object" && data2 !== null && "enabled" in data2 && data2.enabled === true;
2042
+ }
2043
+ function getUrlOrigin(url) {
2044
+ try {
2045
+ return new URL(
2046
+ url,
2047
+ typeof window === "undefined" ? void 0 : window.location.href
2048
+ ).origin;
2049
+ } catch {
2050
+ }
2051
+ }
2052
+ function isLoaderPresent(fileName, injectedFlag) {
2053
+ return Boolean(
2054
+ injectedFlag || document.querySelector(`script[src*="${fileName}"]`)
2055
+ );
2056
+ }
2057
+ function appendLoaderScript({
2058
+ attributes,
2059
+ onLoad,
2060
+ src
2061
+ }) {
2062
+ const script = document.createElement("script");
2063
+ script.async = true;
2064
+ script.src = src;
2065
+ for (const [name, value] of Object.entries(attributes)) {
2066
+ script.setAttribute(name, value);
2067
+ }
2068
+ if (onLoad) {
2069
+ script.addEventListener("load", onLoad);
2070
+ }
2071
+ (document.body ?? document.head ?? document.documentElement).append(script);
2072
+ }
2073
+
1878
2074
  // src/lib/social-proof/storage.ts
1879
2075
  var WIDGET_PAYLOAD_PATH = "/api/social-proof/widget";
1880
2076
  var SOCIAL_PROOF_LOADER_FILE = "saasco-social-proof-loader.js";
@@ -1954,17 +2150,16 @@ async function injectSocialProofLoader({
1954
2150
  return;
1955
2151
  }
1956
2152
  window.__saascoSocialProofInjected = true;
1957
- const script = document.createElement("script");
1958
- script.async = true;
1959
- script.src = normalizeSaascoUrl(scriptUrl);
1960
- script.setAttribute("data-projectId", projectId);
2153
+ const attributes = {
2154
+ "data-projectId": projectId
2155
+ };
1961
2156
  if (baseUrlOverride !== void 0) {
1962
- script.setAttribute(
1963
- "data-base-url",
1964
- normalizeSaascoBaseUrl(baseUrlOverride)
1965
- );
2157
+ attributes["data-base-url"] = normalizeSaascoBaseUrl(baseUrlOverride);
1966
2158
  }
1967
- (document.body ?? document.head ?? document.documentElement).append(script);
2159
+ appendLoaderScript({
2160
+ attributes,
2161
+ src: normalizeSaascoUrl(scriptUrl)
2162
+ });
1968
2163
  }
1969
2164
  async function shouldInjectSocialProof({
1970
2165
  baseUrl,
@@ -1993,23 +2188,12 @@ async function shouldInjectSocialProof({
1993
2188
  writeSession(cacheKey, JSON.stringify({ data: data2, fetchedAt: Date.now() }));
1994
2189
  return isEnabledPayload(data2);
1995
2190
  }
1996
- function isEnabledPayload(data2) {
1997
- return typeof data2 === "object" && data2 !== null && data2.enabled === true;
1998
- }
1999
2191
  function isSocialProofLoaderPresent() {
2000
- return Boolean(
2001
- window.__saascoSocialProofInjected || document.querySelector(`script[src*="${SOCIAL_PROOF_LOADER_FILE}"]`)
2192
+ return isLoaderPresent(
2193
+ SOCIAL_PROOF_LOADER_FILE,
2194
+ window.__saascoSocialProofInjected
2002
2195
  );
2003
2196
  }
2004
- function getUrlOrigin(url) {
2005
- try {
2006
- return new URL(
2007
- url,
2008
- typeof window === "undefined" ? void 0 : window.location.href
2009
- ).origin;
2010
- } catch {
2011
- }
2012
- }
2013
2197
  function isSocialProofQaMode() {
2014
2198
  if (typeof window === "undefined") {
2015
2199
  return false;
@@ -2024,6 +2208,72 @@ function isSocialProofQaMode() {
2024
2208
  }
2025
2209
  }
2026
2210
 
2211
+ // src/lib/support/widgetEnabledCheck.ts
2212
+ var SUPPORT_WIDGET_PATH = "/api/support/widget";
2213
+ var SUPPORT_LOADER_FILE = "saasco-support-loader.js";
2214
+ async function injectSupportLoader({
2215
+ baseUrl: baseUrlOverride,
2216
+ onLoad,
2217
+ placeholder,
2218
+ projectId,
2219
+ scriptUrl
2220
+ }) {
2221
+ if (typeof window === "undefined" || typeof document === "undefined") {
2222
+ return "skipped";
2223
+ }
2224
+ if (isSupportLoaderPresent()) {
2225
+ return "present";
2226
+ }
2227
+ const baseUrl = normalizeSaascoBaseUrl(
2228
+ baseUrlOverride ?? getUrlOrigin(scriptUrl) ?? SAASCO_ORIGIN
2229
+ );
2230
+ const enabled = await shouldInjectSupport({
2231
+ baseUrl,
2232
+ projectId
2233
+ });
2234
+ if (!enabled) {
2235
+ return "skipped";
2236
+ }
2237
+ if (isSupportLoaderPresent()) {
2238
+ return "present";
2239
+ }
2240
+ window.__saascoSupportInjected = true;
2241
+ const attributes = {
2242
+ "data-projectId": projectId
2243
+ };
2244
+ if (baseUrlOverride !== void 0) {
2245
+ attributes["data-base-url"] = normalizeSaascoBaseUrl(baseUrlOverride);
2246
+ }
2247
+ if (placeholder !== void 0) {
2248
+ attributes["data-placeholder"] = placeholder;
2249
+ }
2250
+ appendLoaderScript({
2251
+ attributes,
2252
+ onLoad,
2253
+ src: normalizeSaascoUrl(scriptUrl)
2254
+ });
2255
+ return "injected";
2256
+ }
2257
+ async function shouldInjectSupport({
2258
+ baseUrl,
2259
+ projectId
2260
+ }) {
2261
+ try {
2262
+ const response = await fetch(
2263
+ `${baseUrl}${SUPPORT_WIDGET_PATH}?projectId=${encodeURIComponent(projectId)}`
2264
+ );
2265
+ if (!response.ok) {
2266
+ return false;
2267
+ }
2268
+ return isEnabledPayload(await response.json());
2269
+ } catch {
2270
+ return false;
2271
+ }
2272
+ }
2273
+ function isSupportLoaderPresent() {
2274
+ return isLoaderPresent(SUPPORT_LOADER_FILE, window.__saascoSupportInjected);
2275
+ }
2276
+
2027
2277
  // src/lib/analytics.ts
2028
2278
  function getSupportGlobal() {
2029
2279
  return window.SaascoSupport;
@@ -2033,10 +2283,6 @@ var isServer5 = !isBrowser5;
2033
2283
  var PREF = "saasco-sdk";
2034
2284
  var SESSION_DURATION = 1e3 * 60 * 30;
2035
2285
  var USER_DURATION = 1e3 * 60 * 60 * 24 * 365;
2036
- var SUPPORT_ATTR_MAP = {
2037
- baseUrl: "data-base-url",
2038
- placeholder: "data-placeholder"
2039
- };
2040
2286
  var data = {};
2041
2287
  function getRootDomain(url) {
2042
2288
  if (isServer5) {
@@ -2102,6 +2348,15 @@ function migrateFromLocalStorage() {
2102
2348
  if (isServer5) {
2103
2349
  return;
2104
2350
  }
2351
+ let storage;
2352
+ try {
2353
+ storage = globalThis.localStorage ?? null;
2354
+ } catch {
2355
+ return;
2356
+ }
2357
+ if (!storage) {
2358
+ return;
2359
+ }
2105
2360
  const keysToMigrate = [
2106
2361
  `${PREF}-session-id`,
2107
2362
  `${PREF}-anonymous-id`,
@@ -2111,20 +2366,23 @@ function migrateFromLocalStorage() {
2111
2366
  ];
2112
2367
  for (const key of keysToMigrate) {
2113
2368
  try {
2114
- const value = localStorage.getItem(key);
2369
+ const value = storage.getItem(key);
2115
2370
  if (value) {
2116
2371
  const item = JSON.parse(value);
2117
2372
  const now = Date.now();
2118
2373
  if (item.expiry > now) {
2119
2374
  const remainingTtl = item.expiry - now;
2120
2375
  setCookie(key, JSON.stringify(item.value), remainingTtl);
2121
- localStorage.removeItem(key);
2376
+ storage.removeItem(key);
2122
2377
  } else {
2123
- localStorage.removeItem(key);
2378
+ storage.removeItem(key);
2124
2379
  }
2125
2380
  }
2126
2381
  } catch {
2127
- localStorage.removeItem(key);
2382
+ try {
2383
+ storage.removeItem(key);
2384
+ } catch {
2385
+ }
2128
2386
  }
2129
2387
  }
2130
2388
  }
@@ -2325,7 +2583,7 @@ var Saasco = class {
2325
2583
  * @param config Configuration options.
2326
2584
  * @param config.projectId The unique identifier for the project.
2327
2585
  * @param config.proxy The URL of the proxy server to use, if any.
2328
- * @param config.autoPageTracking Whether to automatically track page views. Default is false.
2586
+ * @param config.autoPageTracking Whether to automatically track page views. Default is true.
2329
2587
  * @param config.enabled Whether analytics is enabled. Default is true. Set to false for development and staging envioronments. Will still allow debug mode to be true, just no events will be sent
2330
2588
  * @param config.debug Whether to log debug information. Default is false.
2331
2589
  * @param config.trackUrlParams Whether to track URL parameters. Default is true.
@@ -2355,7 +2613,8 @@ var Saasco = class {
2355
2613
  };
2356
2614
  this.integrationManager = new IntegrationManager({
2357
2615
  loggerLevel,
2358
- maxQueueSize: 1e3
2616
+ maxQueueSize: 1e3,
2617
+ deduplicationNamespace: this.config.projectId
2359
2618
  });
2360
2619
  this.integrationManager.setContext({});
2361
2620
  void (async () => {
@@ -2378,6 +2637,7 @@ var Saasco = class {
2378
2637
  // here so an `identify()` that fires before the widget bundle finishes
2379
2638
  // loading is replayed once it does.
2380
2639
  lastSupportIdentity = null;
2640
+ ingestFailureKeys = /* @__PURE__ */ new Set();
2381
2641
  init() {
2382
2642
  if (this.isInitialized) {
2383
2643
  this.logger.info(
@@ -2401,52 +2661,20 @@ var Saasco = class {
2401
2661
  );
2402
2662
  }
2403
2663
  this.initAutoPageTracking();
2404
- if (this.config.support && this.config.support.enabled !== false) {
2405
- this.injectSupport();
2406
- }
2407
- if (this.config.socialProof?.enabled !== false) {
2408
- void this.injectSocialProof();
2409
- }
2664
+ void this.injectSupport();
2665
+ void this.injectSocialProof();
2410
2666
  this.isInitialized = true;
2411
2667
  }
2412
2668
  disableDebug() {
2413
2669
  this.logger.info("Debug mode deactivated.");
2414
2670
  this.config.debug = false;
2671
+ this.logger.setLevel(getLoggerLevel(this.config));
2415
2672
  }
2416
2673
  enableDebug() {
2417
2674
  this.config.debug = true;
2675
+ this.logger.setLevel(getLoggerLevel(this.config));
2418
2676
  this.logger.info("Debug mode activated.");
2419
2677
  }
2420
- /**
2421
- * Enables and injects the support widget when it was constructed with
2422
- * `support: { enabled: false }`. Safe to call multiple times.
2423
- */
2424
- enableSupport() {
2425
- if (!this.config.support) {
2426
- this.logger.warn(
2427
- "Support is not configured; enableSupport() was ignored."
2428
- );
2429
- return;
2430
- }
2431
- this.config.support.enabled = true;
2432
- this.injectSupport();
2433
- this.pushSupportIdentity();
2434
- }
2435
- /**
2436
- * Enables and injects the social-proof widget (e.g. after constructing with
2437
- * `socialProof: { enabled: false }`, or when no `socialProof` block was
2438
- * passed). Injection still runs the server check, so the loader only
2439
- * downloads when the project has the app enabled in the dashboard.
2440
- * Safe to call multiple times — the loader injection is guarded against
2441
- * double-injection.
2442
- */
2443
- enableSocialProof() {
2444
- this.config.socialProof = {
2445
- ...this.config.socialProof,
2446
- enabled: true
2447
- };
2448
- void this.injectSocialProof();
2449
- }
2450
2678
  /**
2451
2679
  * Initialize third-party integrations
2452
2680
  */
@@ -2461,48 +2689,52 @@ var Saasco = class {
2461
2689
  );
2462
2690
  return;
2463
2691
  }
2464
- for (const integrationConfig of this.config.integrations) {
2465
- try {
2466
- if (integrationConfig.type === "facebook-pixel") {
2467
- this.logger.info(
2468
- "Registering Facebook Pixel integration with ID:",
2469
- integrationConfig.config.pixelId
2470
- );
2471
- const fbIntegration = createFacebookPixelIntegration(
2472
- integrationConfig.config,
2473
- integrationConfig.debug
2474
- );
2475
- await this.integrationManager.registerIntegration(fbIntegration);
2476
- } else if (integrationConfig.type === "pinterest-tag") {
2477
- this.logger.info(
2478
- "Registering Pinterest Tag integration with ID:",
2479
- integrationConfig.config.tagId
2480
- );
2481
- const pinterestIntegration = createPinterestTagIntegration(
2482
- integrationConfig.config,
2483
- integrationConfig.debug
2484
- );
2485
- await this.integrationManager.registerIntegration(
2486
- pinterestIntegration
2487
- );
2488
- } else if (integrationConfig.type === "tiktok-pixel") {
2692
+ await Promise.all(
2693
+ this.config.integrations.map(async (integrationConfig) => {
2694
+ try {
2695
+ if (integrationConfig.type === "facebook-pixel") {
2696
+ this.logger.info(
2697
+ "Registering Facebook Pixel integration with ID:",
2698
+ integrationConfig.config.pixelId
2699
+ );
2700
+ const fbIntegration = createFacebookPixelIntegration(
2701
+ integrationConfig.config,
2702
+ integrationConfig.debug
2703
+ );
2704
+ await this.integrationManager.registerIntegration(fbIntegration);
2705
+ } else if (integrationConfig.type === "pinterest-tag") {
2706
+ this.logger.info(
2707
+ "Registering Pinterest Tag integration with ID:",
2708
+ integrationConfig.config.tagId
2709
+ );
2710
+ const pinterestIntegration = createPinterestTagIntegration(
2711
+ integrationConfig.config,
2712
+ integrationConfig.debug
2713
+ );
2714
+ await this.integrationManager.registerIntegration(
2715
+ pinterestIntegration
2716
+ );
2717
+ } else if (integrationConfig.type === "tiktok-pixel") {
2718
+ this.logger.info(
2719
+ "Registering TikTok Pixel integration with ID:",
2720
+ integrationConfig.config.pixelId
2721
+ );
2722
+ const tiktokIntegration = createTikTokPixelIntegration(
2723
+ integrationConfig.config,
2724
+ integrationConfig.debug
2725
+ );
2726
+ await this.integrationManager.registerIntegration(
2727
+ tiktokIntegration
2728
+ );
2729
+ }
2730
+ } catch (error) {
2489
2731
  this.logger.info(
2490
- "Registering TikTok Pixel integration with ID:",
2491
- integrationConfig.config.pixelId
2492
- );
2493
- const tiktokIntegration = createTikTokPixelIntegration(
2494
- integrationConfig.config,
2495
- integrationConfig.debug
2732
+ `Failed to register ${integrationConfig.type} integration:`,
2733
+ error
2496
2734
  );
2497
- await this.integrationManager.registerIntegration(tiktokIntegration);
2498
2735
  }
2499
- } catch (error) {
2500
- this.logger.info(
2501
- `Failed to register ${integrationConfig.type} integration:`,
2502
- error
2503
- );
2504
- }
2505
- }
2736
+ })
2737
+ );
2506
2738
  }
2507
2739
  track(actionOrPayload, propertiesOrNothing, contextOrNothing) {
2508
2740
  if (!this.config.projectId) {
@@ -2559,11 +2791,23 @@ var Saasco = class {
2559
2791
  });
2560
2792
  }
2561
2793
  this.integrationManager.track(action, properties);
2562
- return this.doRequest("events", requestData);
2794
+ const eventId = properties?.["event_id"];
2795
+ if (!isBrowser5 || typeof eventId !== "string" || !eventId.trim()) {
2796
+ return this.doRequest("events", requestData);
2797
+ }
2798
+ return deliverOnce(
2799
+ `saasco:${this.config.projectId}:ingest:${action}:${eventId}`,
2800
+ async () => {
2801
+ const result = await this.doRequest("events", requestData);
2802
+ if (!result.success) {
2803
+ throw new Error(result.message);
2804
+ }
2805
+ }
2806
+ ).then(() => ({ success: true, message: "Event dispatched" })).catch((error) => ({ success: false, message: String(error) }));
2563
2807
  }
2564
2808
  /**
2565
2809
  * The page method lets you record page views on your website
2566
- * This records the page title and path and names the event useing the reserved property "Page Viewed"
2810
+ * This records the page title and path and names the event using the reserved name "Page View"
2567
2811
  *
2568
2812
  * Before implementing this make sure you have disabled the autoPageTracking in the config or you will get duplicate page views
2569
2813
  */
@@ -2659,7 +2903,10 @@ var Saasco = class {
2659
2903
  },
2660
2904
  context: context || {}
2661
2905
  };
2662
- this.logger.info("identify", requestData);
2906
+ this.logger.info("identify", {
2907
+ distinctId,
2908
+ projectId: this.config.projectId
2909
+ });
2663
2910
  if (!this.config.enabled) {
2664
2911
  return Promise.resolve({
2665
2912
  success: true,
@@ -2695,6 +2942,13 @@ var Saasco = class {
2695
2942
  message: "Analytics is disabled"
2696
2943
  };
2697
2944
  }
2945
+ const projectId = requestData.projectId ?? null;
2946
+ const fail = (input) => reportIngestFailure({
2947
+ failure: formatAnalyticsRequestFailure({ path, projectId, ...input }),
2948
+ logger: this.logger,
2949
+ path,
2950
+ seen: this.ingestFailureKeys
2951
+ });
2698
2952
  let response;
2699
2953
  try {
2700
2954
  response = await fetch(url, {
@@ -2704,46 +2958,22 @@ var Saasco = class {
2704
2958
  },
2705
2959
  body: JSON.stringify(requestData)
2706
2960
  });
2707
- } catch (error) {
2708
- const message = error instanceof Error ? error.message : String(error);
2709
- this.logger.warn(
2710
- `Network request failed`,
2711
- `
2712
- Error Message: "${message}"`,
2713
- `
2714
- Url: "${url}"`
2715
- );
2716
- return {
2717
- success: false,
2718
- message
2719
- };
2961
+ } catch {
2962
+ return fail({ reason: "unreachable" });
2720
2963
  }
2964
+ let responseBody;
2721
2965
  try {
2722
- const responseBody = await response.json();
2723
- if (!response.ok) {
2724
- throw new Error(
2725
- `${responseBody.message}: ${responseBody.errors ? `[${responseBody.errors.join(", ")}]` : ""}` || JSON.stringify(responseBody)
2726
- );
2727
- }
2728
- return {
2729
- success: true,
2730
- message: responseBody.message ?? ""
2731
- };
2732
- } catch (error) {
2733
- const message = error instanceof Error ? error.message : String(error);
2734
- this.logger.error(
2735
- `
2736
- Error Message: "${message}"`,
2737
- `
2738
- Path: "/${path}"`,
2739
- `
2740
- Request Data: ${JSON.stringify(requestData, null, 2)}`
2741
- );
2742
- return {
2743
- success: false,
2744
- message
2745
- };
2966
+ responseBody = await response.json();
2967
+ } catch {
2968
+ return fail({ reason: "http" });
2969
+ }
2970
+ if (!response.ok) {
2971
+ return fail({ reason: "http", body: responseBody });
2746
2972
  }
2973
+ return {
2974
+ success: true,
2975
+ message: responseBody && typeof responseBody === "object" && "message" in responseBody && typeof responseBody.message === "string" ? responseBody.message : ""
2976
+ };
2747
2977
  }
2748
2978
  /**
2749
2979
  * If autoPageTracking is enabled, this will automatically track page views
@@ -2787,51 +3017,25 @@ Request Data: ${JSON.stringify(requestData, null, 2)}`
2787
3017
  return this.integrationManager.getStats();
2788
3018
  }
2789
3019
  /**
2790
- * Lazily injects the support **loader** from the same `/sdk/` origin as
2791
- * the analytics bundle, forwarding the shared `projectId` and the widget
2792
- * config as `data-*` attributes. The loader (no React) injects the
2793
- * cross-origin embed iframe. Deferred (`async`) and guarded against
2794
- * double-injection so re-running `init()` is a no-op.
3020
+ * Lazily injects the support **loader** (server-gated). See
3021
+ * {@link injectSupportLoader}. Replays identity once the script loads.
2795
3022
  */
2796
- injectSupport() {
2797
- if (!isBrowser5 || typeof document === "undefined") {
2798
- return;
2799
- }
2800
- const chat = this.config.support;
2801
- if (!chat) {
2802
- return;
2803
- }
2804
- if (window.__saascoSupportInjected || document.querySelector('script[src*="saasco-support-loader.js"]')) {
2805
- return;
2806
- }
2807
- const scriptUrl = resolveLoaderScriptUrl(
2808
- "saasco-support-loader.js",
2809
- chat.scriptUrl
2810
- );
2811
- if (!scriptUrl) {
2812
- this.logger.warn(
2813
- "Support is enabled but the loader URL could not be resolved. Pass `support.scriptUrl` pointing at your hosted saasco-support-loader.js (or load the analytics SDK from the CDN)."
2814
- );
3023
+ async injectSupport() {
3024
+ if (!isBrowser5) {
2815
3025
  return;
2816
3026
  }
2817
- window.__saascoSupportInjected = true;
2818
- const script = document.createElement("script");
2819
- script.async = true;
2820
- script.src = normalizeSaascoUrl(scriptUrl);
2821
- script.setAttribute("data-projectId", this.config.projectId);
2822
- for (const [key, attr] of Object.entries(SUPPORT_ATTR_MAP)) {
2823
- const value = chat[key];
2824
- if (value !== void 0) {
2825
- script.setAttribute(
2826
- attr,
2827
- attr === "data-base-url" ? normalizeSaascoBaseUrl(value) : value
2828
- );
2829
- }
2830
- }
2831
- script.addEventListener("load", () => {
2832
- this.pushSupportIdentity();
3027
+ const { support } = this.config;
3028
+ const scriptUrl = resolveLoaderScriptUrl("saasco-support-loader.js", support?.scriptUrl) ?? `${SAASCO_ORIGIN}/sdk/saasco-support-loader.js`;
3029
+ const result = await injectSupportLoader({
3030
+ baseUrl: support?.baseUrl,
3031
+ onLoad: () => this.pushSupportIdentity(),
3032
+ placeholder: support?.placeholder,
3033
+ projectId: this.config.projectId,
3034
+ scriptUrl
2833
3035
  });
2834
- (document.body ?? document.head ?? document.documentElement).append(script);
3036
+ if (result === "present") {
3037
+ this.pushSupportIdentity();
3038
+ }
2835
3039
  }
2836
3040
  /**
2837
3041
  * Lazily injects the social-proof loader (server-gated). See
@@ -2854,27 +3058,23 @@ Request Data: ${JSON.stringify(requestData, null, 2)}`
2854
3058
  }
2855
3059
  /**
2856
3060
  * Records the latest CRM identity and forwards it to the support widget.
2857
- * No-op when support isn't enabled.
2858
3061
  */
2859
3062
  updateSupportIdentity(identity) {
2860
- if (!isBrowser5 || !this.config.support) {
3063
+ if (!isBrowser5) {
2861
3064
  return;
2862
3065
  }
2863
3066
  const hasTraits = identity.traits && Object.keys(identity.traits).length > 0;
2864
3067
  this.lastSupportIdentity = identity.distinctId || identity.email || hasTraits ? identity : null;
2865
- if (this.config.support.enabled === false) {
2866
- return;
2867
- }
2868
3068
  this.pushSupportIdentity();
2869
3069
  }
2870
3070
  /**
2871
3071
  * Pushes the current identity onto `window.SaascoSupport.identify`. The
2872
3072
  * widget bundle publishes that global asynchronously, so this short-polls for
2873
- * it (same approach as tool registration); the load handler also calls this,
2874
- * so a fresh page load with a stored distinctId still identifies the chat.
3073
+ * it (same approach as tool registration). `injectSupport` calls this on
3074
+ * script load (or immediately when the loader is already present).
2875
3075
  */
2876
3076
  pushSupportIdentity() {
2877
- if (!isBrowser5 || !this.config.support || this.config.support.enabled === false) {
3077
+ if (!isBrowser5) {
2878
3078
  return;
2879
3079
  }
2880
3080
  const identity = this.lastSupportIdentity ?? {
@@ -3001,31 +3201,31 @@ function getLoggerLevel(config) {
3001
3201
  }
3002
3202
 
3003
3203
  // ../shared/src/lib/types/browserContext.ts
3004
- var import_zod = require("zod");
3005
- var BrowserContextSchema = import_zod.z.object({
3006
- $href: import_zod.z.string(),
3007
- $locale: import_zod.z.string(),
3008
- $location: import_zod.z.string(),
3009
- $pathname: import_zod.z.string(),
3010
- $referrer: import_zod.z.string(),
3011
- $referringDomain: import_zod.z.string(),
3012
- $screenDPI: import_zod.z.number(),
3013
- $screenHeight: import_zod.z.number(),
3014
- $screenWidth: import_zod.z.number(),
3015
- $title: import_zod.z.string(),
3016
- $userAgent: import_zod.z.string(),
3017
- $utmAdId: import_zod.z.string().nullable(),
3018
- $utmAdSource: import_zod.z.string().nullable(),
3019
- $utmCampaign: import_zod.z.string().nullable(),
3020
- $utmCampaignId: import_zod.z.string().nullable(),
3021
- $utmContent: import_zod.z.string().nullable(),
3022
- $utmCreativeFormat: import_zod.z.string().nullable(),
3023
- $utmId: import_zod.z.string().nullable(),
3024
- $utmMarketingTactic: import_zod.z.string().nullable(),
3025
- $utmMedium: import_zod.z.string().nullable(),
3026
- $utmSource: import_zod.z.string().nullable(),
3027
- $utmSourcePlatform: import_zod.z.string().nullable(),
3028
- $utmTerm: import_zod.z.string().nullable()
3204
+ var import_zod2 = require("zod");
3205
+ var BrowserContextSchema = import_zod2.z.object({
3206
+ $href: import_zod2.z.string(),
3207
+ $locale: import_zod2.z.string(),
3208
+ $location: import_zod2.z.string(),
3209
+ $pathname: import_zod2.z.string(),
3210
+ $referrer: import_zod2.z.string(),
3211
+ $referringDomain: import_zod2.z.string(),
3212
+ $screenDPI: import_zod2.z.number(),
3213
+ $screenHeight: import_zod2.z.number(),
3214
+ $screenWidth: import_zod2.z.number(),
3215
+ $title: import_zod2.z.string(),
3216
+ $userAgent: import_zod2.z.string(),
3217
+ $utmAdId: import_zod2.z.string().nullable(),
3218
+ $utmAdSource: import_zod2.z.string().nullable(),
3219
+ $utmCampaign: import_zod2.z.string().nullable(),
3220
+ $utmCampaignId: import_zod2.z.string().nullable(),
3221
+ $utmContent: import_zod2.z.string().nullable(),
3222
+ $utmCreativeFormat: import_zod2.z.string().nullable(),
3223
+ $utmId: import_zod2.z.string().nullable(),
3224
+ $utmMarketingTactic: import_zod2.z.string().nullable(),
3225
+ $utmMedium: import_zod2.z.string().nullable(),
3226
+ $utmSource: import_zod2.z.string().nullable(),
3227
+ $utmSourcePlatform: import_zod2.z.string().nullable(),
3228
+ $utmTerm: import_zod2.z.string().nullable()
3029
3229
  });
3030
3230
  // Annotate the CommonJS export names for ESM import in node:
3031
3231
  0 && (module.exports = {