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/README.md +1005 -0
- package/dist/index.cjs +621 -421
- package/dist/index.d.cts +33 -50
- package/dist/index.d.ts +33 -50
- package/dist/index.js +621 -421
- package/dist/support.cjs +1546 -957
- package/dist/support.d.cts +20 -9
- package/dist/support.d.ts +20 -9
- package/dist/support.js +1528 -939
- package/package.json +21 -1
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.
|
|
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
|
-
*
|
|
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
|
|
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
|
-
|
|
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
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
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
|
-
}
|
|
745
|
-
|
|
746
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
1243
|
-
|
|
1244
|
-
|
|
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.
|
|
1469
|
+
this.globalQueue.push(envelope);
|
|
1470
|
+
this.flush();
|
|
1252
1471
|
}
|
|
1253
|
-
async deliver(evt) {
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
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
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
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
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
script.setAttribute("data-projectId", projectId);
|
|
2153
|
+
const attributes = {
|
|
2154
|
+
"data-projectId": projectId
|
|
2155
|
+
};
|
|
1961
2156
|
if (baseUrlOverride !== void 0) {
|
|
1962
|
-
|
|
1963
|
-
"data-base-url",
|
|
1964
|
-
normalizeSaascoBaseUrl(baseUrlOverride)
|
|
1965
|
-
);
|
|
2157
|
+
attributes["data-base-url"] = normalizeSaascoBaseUrl(baseUrlOverride);
|
|
1966
2158
|
}
|
|
1967
|
-
(
|
|
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
|
|
2001
|
-
|
|
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 =
|
|
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
|
-
|
|
2376
|
+
storage.removeItem(key);
|
|
2122
2377
|
} else {
|
|
2123
|
-
|
|
2378
|
+
storage.removeItem(key);
|
|
2124
2379
|
}
|
|
2125
2380
|
}
|
|
2126
2381
|
} catch {
|
|
2127
|
-
|
|
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
|
|
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
|
-
|
|
2405
|
-
|
|
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
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
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
|
-
|
|
2491
|
-
|
|
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
|
-
}
|
|
2500
|
-
|
|
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
|
-
|
|
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
|
|
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",
|
|
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
|
|
2708
|
-
|
|
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
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
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**
|
|
2791
|
-
*
|
|
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
|
|
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
|
-
|
|
2818
|
-
const
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
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
|
-
(
|
|
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
|
|
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)
|
|
2874
|
-
*
|
|
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
|
|
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
|
|
3005
|
-
var BrowserContextSchema =
|
|
3006
|
-
$href:
|
|
3007
|
-
$locale:
|
|
3008
|
-
$location:
|
|
3009
|
-
$pathname:
|
|
3010
|
-
$referrer:
|
|
3011
|
-
$referringDomain:
|
|
3012
|
-
$screenDPI:
|
|
3013
|
-
$screenHeight:
|
|
3014
|
-
$screenWidth:
|
|
3015
|
-
$title:
|
|
3016
|
-
$userAgent:
|
|
3017
|
-
$utmAdId:
|
|
3018
|
-
$utmAdSource:
|
|
3019
|
-
$utmCampaign:
|
|
3020
|
-
$utmCampaignId:
|
|
3021
|
-
$utmContent:
|
|
3022
|
-
$utmCreativeFormat:
|
|
3023
|
-
$utmId:
|
|
3024
|
-
$utmMarketingTactic:
|
|
3025
|
-
$utmMedium:
|
|
3026
|
-
$utmSource:
|
|
3027
|
-
$utmSourcePlatform:
|
|
3028
|
-
$utmTerm:
|
|
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 = {
|