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.js
CHANGED
|
@@ -2,7 +2,187 @@
|
|
|
2
2
|
import { isValid, parse } from "psl";
|
|
3
3
|
|
|
4
4
|
// package.json
|
|
5
|
-
var version = "0.2.
|
|
5
|
+
var version = "0.2.6";
|
|
6
|
+
|
|
7
|
+
// src/lib/deliverOnce.ts
|
|
8
|
+
var sent = /* @__PURE__ */ new Set();
|
|
9
|
+
var pending = /* @__PURE__ */ new Map();
|
|
10
|
+
function deliverOnce(key, send) {
|
|
11
|
+
if (!key || typeof window === "undefined") {
|
|
12
|
+
return Promise.resolve().then(send);
|
|
13
|
+
}
|
|
14
|
+
if (sent.has(key)) {
|
|
15
|
+
return Promise.resolve();
|
|
16
|
+
}
|
|
17
|
+
try {
|
|
18
|
+
if (window.sessionStorage.getItem(key)) {
|
|
19
|
+
return Promise.resolve();
|
|
20
|
+
}
|
|
21
|
+
} catch {
|
|
22
|
+
}
|
|
23
|
+
const existing = pending.get(key);
|
|
24
|
+
if (existing) {
|
|
25
|
+
return existing;
|
|
26
|
+
}
|
|
27
|
+
const task = Promise.resolve().then(send).then(() => {
|
|
28
|
+
sent.add(key);
|
|
29
|
+
try {
|
|
30
|
+
window.sessionStorage.setItem(key, "true");
|
|
31
|
+
} catch {
|
|
32
|
+
}
|
|
33
|
+
}).finally(() => pending.delete(key));
|
|
34
|
+
pending.set(key, task);
|
|
35
|
+
return task;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ../shared/src/lib/analyticsIngestError.ts
|
|
39
|
+
import { z } from "zod";
|
|
40
|
+
var IDENTIFY_INGEST_ERROR_SOURCE = "saasco-identify";
|
|
41
|
+
var ANALYTICS_INGEST_ERROR_SOURCE = "saasco-analytics";
|
|
42
|
+
var AnalyticsIngestErrorCodeSchema = z.enum([
|
|
43
|
+
"CREDENTIAL_INVALID",
|
|
44
|
+
"RESOURCE_FORBIDDEN",
|
|
45
|
+
"RESOURCE_UNSPECIFIED",
|
|
46
|
+
"ENVIRONMENT_UNREACHABLE",
|
|
47
|
+
"INGEST_FAILED"
|
|
48
|
+
]);
|
|
49
|
+
var AnalyticsIngestSurfaceSchema = z.enum(["identify", "events"]);
|
|
50
|
+
var AnalyticsIngestErrorSchema = z.object({
|
|
51
|
+
affected: z.string(),
|
|
52
|
+
code: AnalyticsIngestErrorCodeSchema,
|
|
53
|
+
message: z.string(),
|
|
54
|
+
projectId: z.string().nullable(),
|
|
55
|
+
recovery: z.string(),
|
|
56
|
+
resource: z.string().nullable(),
|
|
57
|
+
surface: AnalyticsIngestSurfaceSchema
|
|
58
|
+
});
|
|
59
|
+
var IDENTIFY_AFFECTED = "Saasco identification only: CRM contact properties and identity stitching. Page views, charts, and the host application UI are not this failure.";
|
|
60
|
+
var ANALYTICS_EVENT_AFFECTED = "Saasco event tracking only. The host application UI is not this failure.";
|
|
61
|
+
function analyticsIngestError({
|
|
62
|
+
code,
|
|
63
|
+
message,
|
|
64
|
+
projectId,
|
|
65
|
+
resource,
|
|
66
|
+
surface
|
|
67
|
+
}) {
|
|
68
|
+
const copy = ingestCopy({ code, resource, surface });
|
|
69
|
+
return {
|
|
70
|
+
affected: surface === "identify" ? IDENTIFY_AFFECTED : ANALYTICS_EVENT_AFFECTED,
|
|
71
|
+
code,
|
|
72
|
+
message: message ?? copy.message,
|
|
73
|
+
projectId,
|
|
74
|
+
recovery: copy.recovery,
|
|
75
|
+
resource,
|
|
76
|
+
surface
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
function ingestCopy({
|
|
80
|
+
code,
|
|
81
|
+
resource,
|
|
82
|
+
surface
|
|
83
|
+
}) {
|
|
84
|
+
const named = resource ? `\`${resource}\`` : "the requested analytics resource";
|
|
85
|
+
const didNotComplete = surface === "identify" ? "Identification did not complete." : "Event tracking did not complete.";
|
|
86
|
+
switch (code) {
|
|
87
|
+
case "CREDENTIAL_INVALID": {
|
|
88
|
+
return {
|
|
89
|
+
message: "Saasco analytics credentials are invalid or missing. This is not an application login or permissions problem.",
|
|
90
|
+
recovery: "Check TINYBIRD_ANALYTICS_TOKEN on the Saasco ingest worker. Do not change host-app auth. Charts and UI styling are unrelated."
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
case "RESOURCE_FORBIDDEN": {
|
|
94
|
+
return {
|
|
95
|
+
message: `Saasco analytics is not allowed to use ${named}. ${didNotComplete}`,
|
|
96
|
+
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).`
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
case "RESOURCE_UNSPECIFIED": {
|
|
100
|
+
return {
|
|
101
|
+
message: `Saasco analytics was denied access without a named pipe or datasource. ${didNotComplete}`,
|
|
102
|
+
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."
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
case "ENVIRONMENT_UNREACHABLE": {
|
|
106
|
+
return {
|
|
107
|
+
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.",
|
|
108
|
+
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."
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
default: {
|
|
112
|
+
return {
|
|
113
|
+
message: surface === "identify" ? "Saasco identification could not be completed." : "Saasco event tracking could not be completed.",
|
|
114
|
+
recovery: "Retry. If it keeps failing, this is a Saasco analytics ingest issue, not host-app login or chart configuration."
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
function ingestErrorSource(surface) {
|
|
120
|
+
return surface === "identify" ? IDENTIFY_INGEST_ERROR_SOURCE : ANALYTICS_INGEST_ERROR_SOURCE;
|
|
121
|
+
}
|
|
122
|
+
function redactSecrets(text) {
|
|
123
|
+
return text.replaceAll(/Bearer\s+\S+/giu, "Bearer [redacted]").replaceAll(/\bp\.[A-Za-z0-9._-]+/gu, "[redacted]").replaceAll(/token=[^&\s]+/giu, "token=[redacted]");
|
|
124
|
+
}
|
|
125
|
+
function isUnsafeIngestMessage(text) {
|
|
126
|
+
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(
|
|
127
|
+
text
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// src/lib/formatAnalyticsRequestFailure.ts
|
|
132
|
+
function formatAnalyticsRequestFailure(input) {
|
|
133
|
+
const surface = input.path === "identify" ? "identify" : "events";
|
|
134
|
+
const projectId = input.projectId ?? null;
|
|
135
|
+
if (input.reason === "http") {
|
|
136
|
+
const parsed = AnalyticsIngestErrorSchema.safeParse(input.body);
|
|
137
|
+
if (parsed.success) {
|
|
138
|
+
return parsed.data;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
if (input.reason === "unreachable") {
|
|
142
|
+
return analyticsIngestError({
|
|
143
|
+
code: "ENVIRONMENT_UNREACHABLE",
|
|
144
|
+
projectId,
|
|
145
|
+
resource: null,
|
|
146
|
+
surface
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
const legacy = input.body && typeof input.body === "object" && !Array.isArray(input.body) && "message" in input.body && typeof input.body.message === "string" ? input.body.message : "";
|
|
150
|
+
const safeMessage = redactSecrets(legacy).trim();
|
|
151
|
+
return analyticsIngestError({
|
|
152
|
+
code: "INGEST_FAILED",
|
|
153
|
+
message: safeMessage && !isUnsafeIngestMessage(safeMessage) ? safeMessage : void 0,
|
|
154
|
+
projectId,
|
|
155
|
+
resource: null,
|
|
156
|
+
surface
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
function reportIngestFailure({
|
|
160
|
+
failure,
|
|
161
|
+
logger,
|
|
162
|
+
path,
|
|
163
|
+
seen
|
|
164
|
+
}) {
|
|
165
|
+
const key = `${path}:${failure.code}:${failure.resource ?? ""}`;
|
|
166
|
+
if (!seen.has(key)) {
|
|
167
|
+
seen.add(key);
|
|
168
|
+
logger.warn(...analyticsFailureLogLines(failure, path));
|
|
169
|
+
}
|
|
170
|
+
return {
|
|
171
|
+
success: false,
|
|
172
|
+
message: failure.message
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
function analyticsFailureLogLines(failure, path) {
|
|
176
|
+
const lines = [
|
|
177
|
+
`${ingestErrorSource(failure.surface)} ${path} failed (${failure.code})`,
|
|
178
|
+
failure.message,
|
|
179
|
+
failure.resource ? `Resource: ${failure.resource}` : null,
|
|
180
|
+
failure.projectId ? `Project: ${failure.projectId}` : null,
|
|
181
|
+
`Affected: ${failure.affected}`,
|
|
182
|
+
`Recovery: ${failure.recovery}`
|
|
183
|
+
];
|
|
184
|
+
return lines.filter((line) => line !== null);
|
|
185
|
+
}
|
|
6
186
|
|
|
7
187
|
// src/lib/timezones.ts
|
|
8
188
|
var timezones = {
|
|
@@ -451,6 +631,9 @@ var AnalyticsLogger = class {
|
|
|
451
631
|
constructor(config) {
|
|
452
632
|
this.config = config;
|
|
453
633
|
}
|
|
634
|
+
setLevel(level) {
|
|
635
|
+
this.config.level = level;
|
|
636
|
+
}
|
|
454
637
|
debug(...args) {
|
|
455
638
|
if (this.config.level < 3 /* DEBUG */) {
|
|
456
639
|
return;
|
|
@@ -479,12 +662,10 @@ var AnalyticsLogger = class {
|
|
|
479
662
|
);
|
|
480
663
|
}
|
|
481
664
|
/**
|
|
482
|
-
*
|
|
665
|
+
* Warnings always print. Ingest failures use this so they stay visible when
|
|
666
|
+
* the default log level is ERROR, without `console.error` (Next.js overlays it).
|
|
483
667
|
*/
|
|
484
668
|
warn(...args) {
|
|
485
|
-
if (this.config.level < 1 /* WARN */) {
|
|
486
|
-
return;
|
|
487
|
-
}
|
|
488
669
|
const icon = "\u25CD";
|
|
489
670
|
const message = `${icon} ${this.config.label}`;
|
|
490
671
|
console.warn(
|
|
@@ -531,9 +712,34 @@ function getIntegrationLoggerLevel(integrationName, debug) {
|
|
|
531
712
|
}
|
|
532
713
|
|
|
533
714
|
// src/lib/utils/uuid.ts
|
|
534
|
-
|
|
715
|
+
var HEX = [];
|
|
716
|
+
for (let i = 0; i < 256; i++) {
|
|
717
|
+
HEX[i] = (i + 256).toString(16).slice(1);
|
|
718
|
+
}
|
|
535
719
|
function uuid() {
|
|
536
|
-
|
|
720
|
+
const bytes = randomBytes(16);
|
|
721
|
+
bytes[6] = bytes[6] & 15 | 64;
|
|
722
|
+
bytes[8] = bytes[8] & 63 | 128;
|
|
723
|
+
let out = "";
|
|
724
|
+
for (let i = 0; i < 16; i++) {
|
|
725
|
+
out += HEX[bytes[i]];
|
|
726
|
+
if (i === 3 || i === 5 || i === 7 || i === 9) {
|
|
727
|
+
out += "-";
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
return out;
|
|
731
|
+
}
|
|
732
|
+
function randomBytes(length) {
|
|
733
|
+
const bytes = new Uint8Array(length);
|
|
734
|
+
const webCrypto = globalThis.crypto;
|
|
735
|
+
if (typeof webCrypto?.getRandomValues === "function") {
|
|
736
|
+
webCrypto.getRandomValues(bytes);
|
|
737
|
+
return bytes;
|
|
738
|
+
}
|
|
739
|
+
for (let i = 0; i < length; i++) {
|
|
740
|
+
bytes[i] = Math.random() * 256 | 0;
|
|
741
|
+
}
|
|
742
|
+
return bytes;
|
|
537
743
|
}
|
|
538
744
|
|
|
539
745
|
// src/lib/getBrowserContext.ts
|
|
@@ -605,6 +811,50 @@ function resolveAdParams(params) {
|
|
|
605
811
|
|
|
606
812
|
// src/lib/integrations/facebook-pixel.ts
|
|
607
813
|
import { sha256 } from "js-sha256";
|
|
814
|
+
|
|
815
|
+
// src/lib/integrations/loadPixelScript.ts
|
|
816
|
+
var loads = /* @__PURE__ */ new Map();
|
|
817
|
+
function loadPixelScript(src) {
|
|
818
|
+
const existingLoad = loads.get(src);
|
|
819
|
+
if (existingLoad) {
|
|
820
|
+
return existingLoad;
|
|
821
|
+
}
|
|
822
|
+
const existingScript = Array.from(document.scripts).find(
|
|
823
|
+
(script2) => script2.src === src
|
|
824
|
+
);
|
|
825
|
+
const script = existingScript ?? document.createElement("script");
|
|
826
|
+
const task = new Promise((resolve, reject) => {
|
|
827
|
+
const cleanup = () => {
|
|
828
|
+
clearTimeout(timeout);
|
|
829
|
+
script.removeEventListener("load", loaded);
|
|
830
|
+
script.removeEventListener("error", failed);
|
|
831
|
+
};
|
|
832
|
+
const loaded = () => {
|
|
833
|
+
cleanup();
|
|
834
|
+
resolve();
|
|
835
|
+
};
|
|
836
|
+
const failed = () => {
|
|
837
|
+
cleanup();
|
|
838
|
+
script.remove();
|
|
839
|
+
reject(new Error(`Pixel script unavailable: ${src}`));
|
|
840
|
+
};
|
|
841
|
+
const timeout = setTimeout(failed, 1e4);
|
|
842
|
+
script.addEventListener("load", loaded);
|
|
843
|
+
script.addEventListener("error", failed);
|
|
844
|
+
if (!existingScript) {
|
|
845
|
+
script.async = true;
|
|
846
|
+
script.src = src;
|
|
847
|
+
document.head.append(script);
|
|
848
|
+
}
|
|
849
|
+
}).catch((error) => {
|
|
850
|
+
loads.delete(src);
|
|
851
|
+
throw error;
|
|
852
|
+
});
|
|
853
|
+
loads.set(src, task);
|
|
854
|
+
return task;
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
// src/lib/integrations/facebook-pixel.ts
|
|
608
858
|
var isBrowser2 = typeof window !== "undefined";
|
|
609
859
|
var isServer2 = !isBrowser2;
|
|
610
860
|
var standardFacebookEvents = [
|
|
@@ -646,70 +896,32 @@ function createFacebookPixelIntegration(config, debug) {
|
|
|
646
896
|
return;
|
|
647
897
|
}
|
|
648
898
|
logger.info("Initializing Facebook Pixel", { pixelId: config.pixelId });
|
|
649
|
-
if (window.fbq
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
try {
|
|
656
|
-
(function init(f, b, e, v, n, t, s) {
|
|
657
|
-
if (f.fbq) {
|
|
658
|
-
return;
|
|
659
|
-
}
|
|
660
|
-
n = f.fbq = function fbq2() {
|
|
661
|
-
n.callMethod ? n.callMethod.apply(n, arguments) : n.queue.push(arguments);
|
|
662
|
-
};
|
|
663
|
-
if (!f._fbq) {
|
|
664
|
-
f._fbq = n;
|
|
665
|
-
}
|
|
666
|
-
n.push = n;
|
|
667
|
-
n.loaded = true;
|
|
668
|
-
n.version = "2.0";
|
|
669
|
-
n.queue = [];
|
|
670
|
-
t = b.createElement(e);
|
|
671
|
-
t.async = true;
|
|
672
|
-
t.src = v;
|
|
673
|
-
t.addEventListener("load", () => {
|
|
674
|
-
isPixelReady = true;
|
|
675
|
-
logger.info("Facebook Pixel script loaded successfully");
|
|
676
|
-
resolve();
|
|
677
|
-
});
|
|
678
|
-
t.addEventListener("error", () => {
|
|
679
|
-
logger.error("Failed to load Facebook Pixel script");
|
|
680
|
-
reject(new Error("Failed to load Facebook Pixel script"));
|
|
681
|
-
});
|
|
682
|
-
s = b.getElementsByTagName(e)[0];
|
|
683
|
-
s.parentNode.insertBefore(t, s);
|
|
684
|
-
})(
|
|
685
|
-
window,
|
|
686
|
-
document,
|
|
687
|
-
"script",
|
|
688
|
-
"https://connect.facebook.net/en_US/fbevents.js"
|
|
689
|
-
);
|
|
690
|
-
const { fbq } = window;
|
|
691
|
-
if (!fbq) {
|
|
692
|
-
reject(new Error("Facebook Pixel failed to initialize"));
|
|
693
|
-
return;
|
|
694
|
-
}
|
|
695
|
-
fbq.disablePushState = true;
|
|
696
|
-
fbq.allowDuplicatePageViews = true;
|
|
697
|
-
if (config.automaticConfiguration === false) {
|
|
698
|
-
logger.debug("Disabling automatic configuration");
|
|
699
|
-
fbq("set", "autoConfig", false, config.pixelId);
|
|
700
|
-
}
|
|
701
|
-
logger.debug("Initializing Facebook Pixel with ID", config.pixelId);
|
|
702
|
-
fbq("init", config.pixelId);
|
|
703
|
-
if (typeof fbq === "function") {
|
|
704
|
-
isPixelReady = true;
|
|
705
|
-
logger.info("Facebook Pixel initialized synchronously");
|
|
706
|
-
resolve();
|
|
899
|
+
if (!window.fbq) {
|
|
900
|
+
const fbq2 = (...args) => {
|
|
901
|
+
if (fbq2.callMethod) {
|
|
902
|
+
fbq2.callMethod(...args);
|
|
903
|
+
} else {
|
|
904
|
+
fbq2.queue.push(args);
|
|
707
905
|
}
|
|
708
|
-
}
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
906
|
+
};
|
|
907
|
+
fbq2.queue = [];
|
|
908
|
+
fbq2.push = fbq2;
|
|
909
|
+
fbq2.loaded = true;
|
|
910
|
+
fbq2.version = "2.0";
|
|
911
|
+
window.fbq = fbq2;
|
|
912
|
+
window._fbq = fbq2;
|
|
913
|
+
}
|
|
914
|
+
const { fbq } = window;
|
|
915
|
+
fbq.disablePushState = true;
|
|
916
|
+
fbq.allowDuplicatePageViews = true;
|
|
917
|
+
if (config.automaticConfiguration === false) {
|
|
918
|
+
fbq("set", "autoConfig", false, config.pixelId);
|
|
919
|
+
}
|
|
920
|
+
fbq("init", config.pixelId);
|
|
921
|
+
if (!fbq.callMethod) {
|
|
922
|
+
await loadPixelScript("https://connect.facebook.net/en_US/fbevents.js");
|
|
923
|
+
}
|
|
924
|
+
isPixelReady = true;
|
|
713
925
|
},
|
|
714
926
|
track: (eventName, properties, _context) => {
|
|
715
927
|
const { fbq } = window;
|
|
@@ -717,9 +929,11 @@ function createFacebookPixelIntegration(config, debug) {
|
|
|
717
929
|
logger.warn("Facebook Pixel not ready, skipping track event", {
|
|
718
930
|
eventName
|
|
719
931
|
});
|
|
720
|
-
|
|
932
|
+
throw new Error("Facebook Pixel not ready");
|
|
721
933
|
}
|
|
722
934
|
const fbEventName = getFacebookEventName(eventName, config.eventMapping);
|
|
935
|
+
const eventId = properties?.["event_id"];
|
|
936
|
+
const eventOptions = typeof eventId === "string" && eventId.trim().length > 0 ? { eventID: eventId } : void 0;
|
|
723
937
|
const eventsWithParams = ["Purchase", "StartTrial", "Subscribe"];
|
|
724
938
|
const isStandardEvent = standardFacebookEvents.includes(
|
|
725
939
|
fbEventName
|
|
@@ -747,10 +961,18 @@ function createFacebookPixelIntegration(config, debug) {
|
|
|
747
961
|
event: fbEventName,
|
|
748
962
|
params: fbParams
|
|
749
963
|
});
|
|
750
|
-
|
|
964
|
+
if (eventOptions) {
|
|
965
|
+
fbq(trackType, fbEventName, fbParams, eventOptions);
|
|
966
|
+
} else {
|
|
967
|
+
fbq(trackType, fbEventName, fbParams);
|
|
968
|
+
}
|
|
751
969
|
} else {
|
|
752
970
|
logger.info("Tracking Facebook Pixel event", { event: fbEventName });
|
|
753
|
-
|
|
971
|
+
if (eventOptions) {
|
|
972
|
+
fbq(trackType, fbEventName, {}, eventOptions);
|
|
973
|
+
} else {
|
|
974
|
+
fbq(trackType, fbEventName);
|
|
975
|
+
}
|
|
754
976
|
}
|
|
755
977
|
},
|
|
756
978
|
identify: (userId, properties, _context) => {
|
|
@@ -1104,12 +1326,12 @@ var IntegrationManager = class {
|
|
|
1104
1326
|
globalQueue = [];
|
|
1105
1327
|
config;
|
|
1106
1328
|
logger;
|
|
1107
|
-
initTime;
|
|
1108
1329
|
currentEnvironment;
|
|
1109
1330
|
flushTimer;
|
|
1110
1331
|
unloadHandler;
|
|
1111
1332
|
constructor(config = {}) {
|
|
1112
1333
|
this.config = {
|
|
1334
|
+
deduplicationNamespace: config.deduplicationNamespace ?? "default",
|
|
1113
1335
|
flushInterval: config.flushInterval ?? 5e3,
|
|
1114
1336
|
loggerLevel: config.loggerLevel ?? 0 /* ERROR */,
|
|
1115
1337
|
maxIntegrationWaitTime: config.maxIntegrationWaitTime ?? 1e4,
|
|
@@ -1119,7 +1341,6 @@ var IntegrationManager = class {
|
|
|
1119
1341
|
label: "Saasco Integrations Debug",
|
|
1120
1342
|
level: this.config.loggerLevel
|
|
1121
1343
|
});
|
|
1122
|
-
this.initTime = Date.now();
|
|
1123
1344
|
this.currentEnvironment = typeof window === "undefined" ? "server" : "client";
|
|
1124
1345
|
this.setupPeriodicFlushing();
|
|
1125
1346
|
this.setupUnloadHandler();
|
|
@@ -1196,6 +1417,8 @@ var IntegrationManager = class {
|
|
|
1196
1417
|
*/
|
|
1197
1418
|
send(partial) {
|
|
1198
1419
|
const envelope = {
|
|
1420
|
+
delivered: /* @__PURE__ */ new Set(),
|
|
1421
|
+
pending: /* @__PURE__ */ new Set(),
|
|
1199
1422
|
context: { ...this.context },
|
|
1200
1423
|
id: uuid(),
|
|
1201
1424
|
name: partial.name,
|
|
@@ -1203,65 +1426,55 @@ var IntegrationManager = class {
|
|
|
1203
1426
|
timestamp: Date.now(),
|
|
1204
1427
|
type: partial.type
|
|
1205
1428
|
};
|
|
1206
|
-
if (
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
this.logger.warn("Queue full \u2192 dropped oldest");
|
|
1210
|
-
}
|
|
1211
|
-
this.globalQueue.push(envelope);
|
|
1212
|
-
this.logger.debug(`Queued (${this.globalQueue.length})`, envelope);
|
|
1213
|
-
return;
|
|
1429
|
+
if (this.globalQueue.length >= this.config.maxQueueSize) {
|
|
1430
|
+
this.globalQueue.shift();
|
|
1431
|
+
this.logger.warn("Queue full \u2192 dropped oldest");
|
|
1214
1432
|
}
|
|
1215
|
-
this.
|
|
1433
|
+
this.globalQueue.push(envelope);
|
|
1434
|
+
this.flush();
|
|
1216
1435
|
}
|
|
1217
|
-
async deliver(evt) {
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
await integration.identify(
|
|
1236
|
-
evt.context.distinctId,
|
|
1237
|
-
evt.properties,
|
|
1238
|
-
evt.context
|
|
1239
|
-
);
|
|
1240
|
-
}
|
|
1241
|
-
} catch (error) {
|
|
1242
|
-
this.logger.error(
|
|
1243
|
-
`deliver error in ${integration.name} (${evt.type})`,
|
|
1244
|
-
error
|
|
1436
|
+
async deliver(evt, state) {
|
|
1437
|
+
const { integration } = state;
|
|
1438
|
+
evt.pending.add(integration.name);
|
|
1439
|
+
try {
|
|
1440
|
+
if (evt.type === "track" && integration.track && evt.name) {
|
|
1441
|
+
const eventId = evt.properties?.["event_id"];
|
|
1442
|
+
const key = typeof eventId === "string" && eventId.trim() ? `saasco:${this.config.deduplicationNamespace}:${integration.name}:${evt.name}:${eventId}` : void 0;
|
|
1443
|
+
const { track } = integration;
|
|
1444
|
+
const { name } = evt;
|
|
1445
|
+
await deliverOnce(
|
|
1446
|
+
key,
|
|
1447
|
+
() => track.call(integration, name, evt.properties, evt.context)
|
|
1448
|
+
);
|
|
1449
|
+
} else if (evt.type === "identify" && integration.identify) {
|
|
1450
|
+
await integration.identify(
|
|
1451
|
+
evt.context.distinctId,
|
|
1452
|
+
evt.properties,
|
|
1453
|
+
evt.context
|
|
1245
1454
|
);
|
|
1246
1455
|
}
|
|
1456
|
+
evt.delivered.add(integration.name);
|
|
1457
|
+
} catch (error) {
|
|
1458
|
+
this.logger.error(
|
|
1459
|
+
`deliver error in ${integration.name} (${evt.type})`,
|
|
1460
|
+
error
|
|
1461
|
+
);
|
|
1462
|
+
} finally {
|
|
1463
|
+
evt.pending.delete(integration.name);
|
|
1247
1464
|
}
|
|
1248
1465
|
}
|
|
1249
|
-
/**
|
|
1250
|
-
*
|
|
1251
|
-
*/
|
|
1466
|
+
/** Each ready destination drains its own copy; a slow/blocked pixel cannot
|
|
1467
|
+
* discard another pixel's queued events or hold up the ready destinations. */
|
|
1252
1468
|
flush() {
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
for (const evt of toSend) {
|
|
1263
|
-
this.deliver(evt);
|
|
1264
|
-
}
|
|
1469
|
+
this.globalQueue = this.globalQueue.filter((evt) => {
|
|
1470
|
+
for (const state of this.integrations.values()) {
|
|
1471
|
+
const { name } = state.integration;
|
|
1472
|
+
if (state.status === "ready" && !evt.delivered.has(name) && !evt.pending.has(name)) {
|
|
1473
|
+
void this.deliver(evt, state);
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
1476
|
+
return evt.delivered.size < this.integrations.size && Date.now() - evt.timestamp < this.config.maxIntegrationWaitTime;
|
|
1477
|
+
});
|
|
1265
1478
|
}
|
|
1266
1479
|
readyCount() {
|
|
1267
1480
|
let n = 0;
|
|
@@ -1272,18 +1485,6 @@ var IntegrationManager = class {
|
|
|
1272
1485
|
}
|
|
1273
1486
|
return n;
|
|
1274
1487
|
}
|
|
1275
|
-
isReady() {
|
|
1276
|
-
const timeElapsed = Date.now() - this.initTime;
|
|
1277
|
-
if (this.readyCount() < this.integrations.size && timeElapsed < this.config.maxIntegrationWaitTime) {
|
|
1278
|
-
return false;
|
|
1279
|
-
}
|
|
1280
|
-
if (this.readyCount() < this.integrations.size) {
|
|
1281
|
-
this.logger.info(
|
|
1282
|
-
`proceeding with ${this.readyCount()}/${this.integrations.size} integrations ready after ${timeElapsed}ms wait`
|
|
1283
|
-
);
|
|
1284
|
-
}
|
|
1285
|
-
return true;
|
|
1286
|
-
}
|
|
1287
1488
|
/**
|
|
1288
1489
|
* Setup periodic flushing if enabled
|
|
1289
1490
|
*/
|
|
@@ -1367,50 +1568,20 @@ function createPinterestTagIntegration(config, debug) {
|
|
|
1367
1568
|
return;
|
|
1368
1569
|
}
|
|
1369
1570
|
logger.info("Initializing Pinterest Tag", { tagId: config.tagId });
|
|
1370
|
-
if (window.pintrk
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1571
|
+
if (!window.pintrk) {
|
|
1572
|
+
const pintrk = (...args) => pintrk.queue.push(args);
|
|
1573
|
+
pintrk.queue = [];
|
|
1574
|
+
pintrk.version = "3.0";
|
|
1575
|
+
window.pintrk = pintrk;
|
|
1374
1576
|
}
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
window.pintrk = function pintrk(...args) {
|
|
1380
|
-
window.pintrk.queue.push(args);
|
|
1381
|
-
};
|
|
1382
|
-
const n = window.pintrk;
|
|
1383
|
-
n.queue = [];
|
|
1384
|
-
n.version = "3.0";
|
|
1385
|
-
const t = document.createElement("script");
|
|
1386
|
-
t.async = true;
|
|
1387
|
-
t.src = e;
|
|
1388
|
-
t.addEventListener("load", () => {
|
|
1389
|
-
window.pintrk("load", config.tagId);
|
|
1390
|
-
window.pintrk("page");
|
|
1391
|
-
setTimeout(() => {
|
|
1392
|
-
logger.debug("Pinterest Tag initialized");
|
|
1393
|
-
isTagReady = true;
|
|
1394
|
-
resolve();
|
|
1395
|
-
}, 500);
|
|
1396
|
-
});
|
|
1397
|
-
t.addEventListener("error", () => {
|
|
1398
|
-
logger.error("Failed to load Pinterest Tag script");
|
|
1399
|
-
reject(new Error("Failed to load Pinterest Tag script"));
|
|
1400
|
-
});
|
|
1401
|
-
const r = document.getElementsByTagName("script")[0];
|
|
1402
|
-
r.parentNode.insertBefore(t, r);
|
|
1403
|
-
}
|
|
1404
|
-
})("https://s.pinimg.com/ct/core.js");
|
|
1405
|
-
} catch (error) {
|
|
1406
|
-
logger.error("Failed to initialize Pinterest Tag", error);
|
|
1407
|
-
reject(error);
|
|
1408
|
-
}
|
|
1409
|
-
});
|
|
1577
|
+
await loadPixelScript("https://s.pinimg.com/ct/core.js");
|
|
1578
|
+
window.pintrk("load", config.tagId);
|
|
1579
|
+
window.pintrk("page");
|
|
1580
|
+
isTagReady = true;
|
|
1410
1581
|
},
|
|
1411
1582
|
track: (eventName, properties) => {
|
|
1412
1583
|
if (!isTagReady || !window.pintrk) {
|
|
1413
|
-
|
|
1584
|
+
throw new Error("Pinterest Tag not ready");
|
|
1414
1585
|
}
|
|
1415
1586
|
const pinterestEventName = getPinterestEventName(
|
|
1416
1587
|
eventName,
|
|
@@ -1539,8 +1710,8 @@ function createTikTokPixelIntegration(config, debug) {
|
|
|
1539
1710
|
return;
|
|
1540
1711
|
}
|
|
1541
1712
|
logger.info("Initializing TikTok Pixel", { pixelId: config.pixelId });
|
|
1542
|
-
if (window.ttq) {
|
|
1543
|
-
|
|
1713
|
+
if (window.ttq?.ready) {
|
|
1714
|
+
await new Promise((resolve) => window.ttq.ready(resolve));
|
|
1544
1715
|
isPixelReady = true;
|
|
1545
1716
|
return;
|
|
1546
1717
|
}
|
|
@@ -1592,28 +1763,15 @@ function createTikTokPixelIntegration(config, debug) {
|
|
|
1592
1763
|
ttq._t[e] = Date.now();
|
|
1593
1764
|
ttq._o ||= {};
|
|
1594
1765
|
ttq._o[e] = n || {};
|
|
1595
|
-
|
|
1596
|
-
script.type = "text/javascript";
|
|
1597
|
-
script.async = true;
|
|
1598
|
-
script.src = `${r}?sdkid=${e}&lib=${t}`;
|
|
1599
|
-
script.addEventListener("load", () => {
|
|
1766
|
+
loadPixelScript(`${r}?sdkid=${e}&lib=${t}`).then(() => {
|
|
1600
1767
|
isPixelReady = true;
|
|
1601
1768
|
resolve();
|
|
1602
|
-
});
|
|
1603
|
-
script.addEventListener("error", () => {
|
|
1604
|
-
reject(new Error("Failed to load TikTok Pixel script"));
|
|
1605
|
-
});
|
|
1606
|
-
const firstScript = document.getElementsByTagName("script")[0];
|
|
1607
|
-
firstScript.parentNode.insertBefore(script, firstScript);
|
|
1769
|
+
}).catch(reject);
|
|
1608
1770
|
};
|
|
1609
1771
|
ttq.load(
|
|
1610
1772
|
config.pixelId,
|
|
1611
1773
|
config.testMode ? { test_mode: true } : void 0
|
|
1612
1774
|
);
|
|
1613
|
-
if (window.ttq && typeof window.ttq === "function") {
|
|
1614
|
-
isPixelReady = true;
|
|
1615
|
-
resolve();
|
|
1616
|
-
}
|
|
1617
1775
|
})(window, document, "ttq");
|
|
1618
1776
|
} catch (error) {
|
|
1619
1777
|
reject(error);
|
|
@@ -1622,8 +1780,8 @@ function createTikTokPixelIntegration(config, debug) {
|
|
|
1622
1780
|
},
|
|
1623
1781
|
track: (eventName, properties, _context) => {
|
|
1624
1782
|
const { ttq } = window;
|
|
1625
|
-
if (!isPixelReady || !ttq) {
|
|
1626
|
-
|
|
1783
|
+
if (!isPixelReady || !ttq?.track) {
|
|
1784
|
+
throw new Error("TikTok Pixel not ready");
|
|
1627
1785
|
}
|
|
1628
1786
|
const ttEventName = getTikTokEventName(eventName, config.eventMapping);
|
|
1629
1787
|
if (ttEventName === "PageView") {
|
|
@@ -1631,7 +1789,10 @@ function createTikTokPixelIntegration(config, debug) {
|
|
|
1631
1789
|
return;
|
|
1632
1790
|
}
|
|
1633
1791
|
const ttParams = mapTikTokEventParameters(ttEventName, properties);
|
|
1634
|
-
|
|
1792
|
+
const eventId = properties?.["event_id"];
|
|
1793
|
+
if (typeof eventId === "string" && eventId.trim().length > 0) {
|
|
1794
|
+
ttq.track?.(ttEventName, ttParams ?? {}, { event_id: eventId });
|
|
1795
|
+
} else if (ttParams && Object.keys(ttParams).length > 0) {
|
|
1635
1796
|
ttq.track?.(ttEventName, ttParams);
|
|
1636
1797
|
} else {
|
|
1637
1798
|
ttq.track?.(ttEventName);
|
|
@@ -1839,6 +2000,41 @@ function normalizeSaascoUrl(url) {
|
|
|
1839
2000
|
return parsed.toString();
|
|
1840
2001
|
}
|
|
1841
2002
|
|
|
2003
|
+
// src/lib/widgetLoader.ts
|
|
2004
|
+
function isEnabledPayload(data2) {
|
|
2005
|
+
return typeof data2 === "object" && data2 !== null && "enabled" in data2 && data2.enabled === true;
|
|
2006
|
+
}
|
|
2007
|
+
function getUrlOrigin(url) {
|
|
2008
|
+
try {
|
|
2009
|
+
return new URL(
|
|
2010
|
+
url,
|
|
2011
|
+
typeof window === "undefined" ? void 0 : window.location.href
|
|
2012
|
+
).origin;
|
|
2013
|
+
} catch {
|
|
2014
|
+
}
|
|
2015
|
+
}
|
|
2016
|
+
function isLoaderPresent(fileName, injectedFlag) {
|
|
2017
|
+
return Boolean(
|
|
2018
|
+
injectedFlag || document.querySelector(`script[src*="${fileName}"]`)
|
|
2019
|
+
);
|
|
2020
|
+
}
|
|
2021
|
+
function appendLoaderScript({
|
|
2022
|
+
attributes,
|
|
2023
|
+
onLoad,
|
|
2024
|
+
src
|
|
2025
|
+
}) {
|
|
2026
|
+
const script = document.createElement("script");
|
|
2027
|
+
script.async = true;
|
|
2028
|
+
script.src = src;
|
|
2029
|
+
for (const [name, value] of Object.entries(attributes)) {
|
|
2030
|
+
script.setAttribute(name, value);
|
|
2031
|
+
}
|
|
2032
|
+
if (onLoad) {
|
|
2033
|
+
script.addEventListener("load", onLoad);
|
|
2034
|
+
}
|
|
2035
|
+
(document.body ?? document.head ?? document.documentElement).append(script);
|
|
2036
|
+
}
|
|
2037
|
+
|
|
1842
2038
|
// src/lib/social-proof/storage.ts
|
|
1843
2039
|
var WIDGET_PAYLOAD_PATH = "/api/social-proof/widget";
|
|
1844
2040
|
var SOCIAL_PROOF_LOADER_FILE = "saasco-social-proof-loader.js";
|
|
@@ -1918,17 +2114,16 @@ async function injectSocialProofLoader({
|
|
|
1918
2114
|
return;
|
|
1919
2115
|
}
|
|
1920
2116
|
window.__saascoSocialProofInjected = true;
|
|
1921
|
-
const
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
script.setAttribute("data-projectId", projectId);
|
|
2117
|
+
const attributes = {
|
|
2118
|
+
"data-projectId": projectId
|
|
2119
|
+
};
|
|
1925
2120
|
if (baseUrlOverride !== void 0) {
|
|
1926
|
-
|
|
1927
|
-
"data-base-url",
|
|
1928
|
-
normalizeSaascoBaseUrl(baseUrlOverride)
|
|
1929
|
-
);
|
|
2121
|
+
attributes["data-base-url"] = normalizeSaascoBaseUrl(baseUrlOverride);
|
|
1930
2122
|
}
|
|
1931
|
-
(
|
|
2123
|
+
appendLoaderScript({
|
|
2124
|
+
attributes,
|
|
2125
|
+
src: normalizeSaascoUrl(scriptUrl)
|
|
2126
|
+
});
|
|
1932
2127
|
}
|
|
1933
2128
|
async function shouldInjectSocialProof({
|
|
1934
2129
|
baseUrl,
|
|
@@ -1957,23 +2152,12 @@ async function shouldInjectSocialProof({
|
|
|
1957
2152
|
writeSession(cacheKey, JSON.stringify({ data: data2, fetchedAt: Date.now() }));
|
|
1958
2153
|
return isEnabledPayload(data2);
|
|
1959
2154
|
}
|
|
1960
|
-
function isEnabledPayload(data2) {
|
|
1961
|
-
return typeof data2 === "object" && data2 !== null && data2.enabled === true;
|
|
1962
|
-
}
|
|
1963
2155
|
function isSocialProofLoaderPresent() {
|
|
1964
|
-
return
|
|
1965
|
-
|
|
2156
|
+
return isLoaderPresent(
|
|
2157
|
+
SOCIAL_PROOF_LOADER_FILE,
|
|
2158
|
+
window.__saascoSocialProofInjected
|
|
1966
2159
|
);
|
|
1967
2160
|
}
|
|
1968
|
-
function getUrlOrigin(url) {
|
|
1969
|
-
try {
|
|
1970
|
-
return new URL(
|
|
1971
|
-
url,
|
|
1972
|
-
typeof window === "undefined" ? void 0 : window.location.href
|
|
1973
|
-
).origin;
|
|
1974
|
-
} catch {
|
|
1975
|
-
}
|
|
1976
|
-
}
|
|
1977
2161
|
function isSocialProofQaMode() {
|
|
1978
2162
|
if (typeof window === "undefined") {
|
|
1979
2163
|
return false;
|
|
@@ -1988,6 +2172,72 @@ function isSocialProofQaMode() {
|
|
|
1988
2172
|
}
|
|
1989
2173
|
}
|
|
1990
2174
|
|
|
2175
|
+
// src/lib/support/widgetEnabledCheck.ts
|
|
2176
|
+
var SUPPORT_WIDGET_PATH = "/api/support/widget";
|
|
2177
|
+
var SUPPORT_LOADER_FILE = "saasco-support-loader.js";
|
|
2178
|
+
async function injectSupportLoader({
|
|
2179
|
+
baseUrl: baseUrlOverride,
|
|
2180
|
+
onLoad,
|
|
2181
|
+
placeholder,
|
|
2182
|
+
projectId,
|
|
2183
|
+
scriptUrl
|
|
2184
|
+
}) {
|
|
2185
|
+
if (typeof window === "undefined" || typeof document === "undefined") {
|
|
2186
|
+
return "skipped";
|
|
2187
|
+
}
|
|
2188
|
+
if (isSupportLoaderPresent()) {
|
|
2189
|
+
return "present";
|
|
2190
|
+
}
|
|
2191
|
+
const baseUrl = normalizeSaascoBaseUrl(
|
|
2192
|
+
baseUrlOverride ?? getUrlOrigin(scriptUrl) ?? SAASCO_ORIGIN
|
|
2193
|
+
);
|
|
2194
|
+
const enabled = await shouldInjectSupport({
|
|
2195
|
+
baseUrl,
|
|
2196
|
+
projectId
|
|
2197
|
+
});
|
|
2198
|
+
if (!enabled) {
|
|
2199
|
+
return "skipped";
|
|
2200
|
+
}
|
|
2201
|
+
if (isSupportLoaderPresent()) {
|
|
2202
|
+
return "present";
|
|
2203
|
+
}
|
|
2204
|
+
window.__saascoSupportInjected = true;
|
|
2205
|
+
const attributes = {
|
|
2206
|
+
"data-projectId": projectId
|
|
2207
|
+
};
|
|
2208
|
+
if (baseUrlOverride !== void 0) {
|
|
2209
|
+
attributes["data-base-url"] = normalizeSaascoBaseUrl(baseUrlOverride);
|
|
2210
|
+
}
|
|
2211
|
+
if (placeholder !== void 0) {
|
|
2212
|
+
attributes["data-placeholder"] = placeholder;
|
|
2213
|
+
}
|
|
2214
|
+
appendLoaderScript({
|
|
2215
|
+
attributes,
|
|
2216
|
+
onLoad,
|
|
2217
|
+
src: normalizeSaascoUrl(scriptUrl)
|
|
2218
|
+
});
|
|
2219
|
+
return "injected";
|
|
2220
|
+
}
|
|
2221
|
+
async function shouldInjectSupport({
|
|
2222
|
+
baseUrl,
|
|
2223
|
+
projectId
|
|
2224
|
+
}) {
|
|
2225
|
+
try {
|
|
2226
|
+
const response = await fetch(
|
|
2227
|
+
`${baseUrl}${SUPPORT_WIDGET_PATH}?projectId=${encodeURIComponent(projectId)}`
|
|
2228
|
+
);
|
|
2229
|
+
if (!response.ok) {
|
|
2230
|
+
return false;
|
|
2231
|
+
}
|
|
2232
|
+
return isEnabledPayload(await response.json());
|
|
2233
|
+
} catch {
|
|
2234
|
+
return false;
|
|
2235
|
+
}
|
|
2236
|
+
}
|
|
2237
|
+
function isSupportLoaderPresent() {
|
|
2238
|
+
return isLoaderPresent(SUPPORT_LOADER_FILE, window.__saascoSupportInjected);
|
|
2239
|
+
}
|
|
2240
|
+
|
|
1991
2241
|
// src/lib/analytics.ts
|
|
1992
2242
|
function getSupportGlobal() {
|
|
1993
2243
|
return window.SaascoSupport;
|
|
@@ -1997,10 +2247,6 @@ var isServer5 = !isBrowser5;
|
|
|
1997
2247
|
var PREF = "saasco-sdk";
|
|
1998
2248
|
var SESSION_DURATION = 1e3 * 60 * 30;
|
|
1999
2249
|
var USER_DURATION = 1e3 * 60 * 60 * 24 * 365;
|
|
2000
|
-
var SUPPORT_ATTR_MAP = {
|
|
2001
|
-
baseUrl: "data-base-url",
|
|
2002
|
-
placeholder: "data-placeholder"
|
|
2003
|
-
};
|
|
2004
2250
|
var data = {};
|
|
2005
2251
|
function getRootDomain(url) {
|
|
2006
2252
|
if (isServer5) {
|
|
@@ -2066,6 +2312,15 @@ function migrateFromLocalStorage() {
|
|
|
2066
2312
|
if (isServer5) {
|
|
2067
2313
|
return;
|
|
2068
2314
|
}
|
|
2315
|
+
let storage;
|
|
2316
|
+
try {
|
|
2317
|
+
storage = globalThis.localStorage ?? null;
|
|
2318
|
+
} catch {
|
|
2319
|
+
return;
|
|
2320
|
+
}
|
|
2321
|
+
if (!storage) {
|
|
2322
|
+
return;
|
|
2323
|
+
}
|
|
2069
2324
|
const keysToMigrate = [
|
|
2070
2325
|
`${PREF}-session-id`,
|
|
2071
2326
|
`${PREF}-anonymous-id`,
|
|
@@ -2075,20 +2330,23 @@ function migrateFromLocalStorage() {
|
|
|
2075
2330
|
];
|
|
2076
2331
|
for (const key of keysToMigrate) {
|
|
2077
2332
|
try {
|
|
2078
|
-
const value =
|
|
2333
|
+
const value = storage.getItem(key);
|
|
2079
2334
|
if (value) {
|
|
2080
2335
|
const item = JSON.parse(value);
|
|
2081
2336
|
const now = Date.now();
|
|
2082
2337
|
if (item.expiry > now) {
|
|
2083
2338
|
const remainingTtl = item.expiry - now;
|
|
2084
2339
|
setCookie(key, JSON.stringify(item.value), remainingTtl);
|
|
2085
|
-
|
|
2340
|
+
storage.removeItem(key);
|
|
2086
2341
|
} else {
|
|
2087
|
-
|
|
2342
|
+
storage.removeItem(key);
|
|
2088
2343
|
}
|
|
2089
2344
|
}
|
|
2090
2345
|
} catch {
|
|
2091
|
-
|
|
2346
|
+
try {
|
|
2347
|
+
storage.removeItem(key);
|
|
2348
|
+
} catch {
|
|
2349
|
+
}
|
|
2092
2350
|
}
|
|
2093
2351
|
}
|
|
2094
2352
|
}
|
|
@@ -2289,7 +2547,7 @@ var Saasco = class {
|
|
|
2289
2547
|
* @param config Configuration options.
|
|
2290
2548
|
* @param config.projectId The unique identifier for the project.
|
|
2291
2549
|
* @param config.proxy The URL of the proxy server to use, if any.
|
|
2292
|
-
* @param config.autoPageTracking Whether to automatically track page views. Default is
|
|
2550
|
+
* @param config.autoPageTracking Whether to automatically track page views. Default is true.
|
|
2293
2551
|
* @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
|
|
2294
2552
|
* @param config.debug Whether to log debug information. Default is false.
|
|
2295
2553
|
* @param config.trackUrlParams Whether to track URL parameters. Default is true.
|
|
@@ -2319,7 +2577,8 @@ var Saasco = class {
|
|
|
2319
2577
|
};
|
|
2320
2578
|
this.integrationManager = new IntegrationManager({
|
|
2321
2579
|
loggerLevel,
|
|
2322
|
-
maxQueueSize: 1e3
|
|
2580
|
+
maxQueueSize: 1e3,
|
|
2581
|
+
deduplicationNamespace: this.config.projectId
|
|
2323
2582
|
});
|
|
2324
2583
|
this.integrationManager.setContext({});
|
|
2325
2584
|
void (async () => {
|
|
@@ -2342,6 +2601,7 @@ var Saasco = class {
|
|
|
2342
2601
|
// here so an `identify()` that fires before the widget bundle finishes
|
|
2343
2602
|
// loading is replayed once it does.
|
|
2344
2603
|
lastSupportIdentity = null;
|
|
2604
|
+
ingestFailureKeys = /* @__PURE__ */ new Set();
|
|
2345
2605
|
init() {
|
|
2346
2606
|
if (this.isInitialized) {
|
|
2347
2607
|
this.logger.info(
|
|
@@ -2365,52 +2625,20 @@ var Saasco = class {
|
|
|
2365
2625
|
);
|
|
2366
2626
|
}
|
|
2367
2627
|
this.initAutoPageTracking();
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
}
|
|
2371
|
-
if (this.config.socialProof?.enabled !== false) {
|
|
2372
|
-
void this.injectSocialProof();
|
|
2373
|
-
}
|
|
2628
|
+
void this.injectSupport();
|
|
2629
|
+
void this.injectSocialProof();
|
|
2374
2630
|
this.isInitialized = true;
|
|
2375
2631
|
}
|
|
2376
2632
|
disableDebug() {
|
|
2377
2633
|
this.logger.info("Debug mode deactivated.");
|
|
2378
2634
|
this.config.debug = false;
|
|
2635
|
+
this.logger.setLevel(getLoggerLevel(this.config));
|
|
2379
2636
|
}
|
|
2380
2637
|
enableDebug() {
|
|
2381
2638
|
this.config.debug = true;
|
|
2639
|
+
this.logger.setLevel(getLoggerLevel(this.config));
|
|
2382
2640
|
this.logger.info("Debug mode activated.");
|
|
2383
2641
|
}
|
|
2384
|
-
/**
|
|
2385
|
-
* Enables and injects the support widget when it was constructed with
|
|
2386
|
-
* `support: { enabled: false }`. Safe to call multiple times.
|
|
2387
|
-
*/
|
|
2388
|
-
enableSupport() {
|
|
2389
|
-
if (!this.config.support) {
|
|
2390
|
-
this.logger.warn(
|
|
2391
|
-
"Support is not configured; enableSupport() was ignored."
|
|
2392
|
-
);
|
|
2393
|
-
return;
|
|
2394
|
-
}
|
|
2395
|
-
this.config.support.enabled = true;
|
|
2396
|
-
this.injectSupport();
|
|
2397
|
-
this.pushSupportIdentity();
|
|
2398
|
-
}
|
|
2399
|
-
/**
|
|
2400
|
-
* Enables and injects the social-proof widget (e.g. after constructing with
|
|
2401
|
-
* `socialProof: { enabled: false }`, or when no `socialProof` block was
|
|
2402
|
-
* passed). Injection still runs the server check, so the loader only
|
|
2403
|
-
* downloads when the project has the app enabled in the dashboard.
|
|
2404
|
-
* Safe to call multiple times — the loader injection is guarded against
|
|
2405
|
-
* double-injection.
|
|
2406
|
-
*/
|
|
2407
|
-
enableSocialProof() {
|
|
2408
|
-
this.config.socialProof = {
|
|
2409
|
-
...this.config.socialProof,
|
|
2410
|
-
enabled: true
|
|
2411
|
-
};
|
|
2412
|
-
void this.injectSocialProof();
|
|
2413
|
-
}
|
|
2414
2642
|
/**
|
|
2415
2643
|
* Initialize third-party integrations
|
|
2416
2644
|
*/
|
|
@@ -2425,48 +2653,52 @@ var Saasco = class {
|
|
|
2425
2653
|
);
|
|
2426
2654
|
return;
|
|
2427
2655
|
}
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2656
|
+
await Promise.all(
|
|
2657
|
+
this.config.integrations.map(async (integrationConfig) => {
|
|
2658
|
+
try {
|
|
2659
|
+
if (integrationConfig.type === "facebook-pixel") {
|
|
2660
|
+
this.logger.info(
|
|
2661
|
+
"Registering Facebook Pixel integration with ID:",
|
|
2662
|
+
integrationConfig.config.pixelId
|
|
2663
|
+
);
|
|
2664
|
+
const fbIntegration = createFacebookPixelIntegration(
|
|
2665
|
+
integrationConfig.config,
|
|
2666
|
+
integrationConfig.debug
|
|
2667
|
+
);
|
|
2668
|
+
await this.integrationManager.registerIntegration(fbIntegration);
|
|
2669
|
+
} else if (integrationConfig.type === "pinterest-tag") {
|
|
2670
|
+
this.logger.info(
|
|
2671
|
+
"Registering Pinterest Tag integration with ID:",
|
|
2672
|
+
integrationConfig.config.tagId
|
|
2673
|
+
);
|
|
2674
|
+
const pinterestIntegration = createPinterestTagIntegration(
|
|
2675
|
+
integrationConfig.config,
|
|
2676
|
+
integrationConfig.debug
|
|
2677
|
+
);
|
|
2678
|
+
await this.integrationManager.registerIntegration(
|
|
2679
|
+
pinterestIntegration
|
|
2680
|
+
);
|
|
2681
|
+
} else if (integrationConfig.type === "tiktok-pixel") {
|
|
2682
|
+
this.logger.info(
|
|
2683
|
+
"Registering TikTok Pixel integration with ID:",
|
|
2684
|
+
integrationConfig.config.pixelId
|
|
2685
|
+
);
|
|
2686
|
+
const tiktokIntegration = createTikTokPixelIntegration(
|
|
2687
|
+
integrationConfig.config,
|
|
2688
|
+
integrationConfig.debug
|
|
2689
|
+
);
|
|
2690
|
+
await this.integrationManager.registerIntegration(
|
|
2691
|
+
tiktokIntegration
|
|
2692
|
+
);
|
|
2693
|
+
}
|
|
2694
|
+
} catch (error) {
|
|
2453
2695
|
this.logger.info(
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
);
|
|
2457
|
-
const tiktokIntegration = createTikTokPixelIntegration(
|
|
2458
|
-
integrationConfig.config,
|
|
2459
|
-
integrationConfig.debug
|
|
2696
|
+
`Failed to register ${integrationConfig.type} integration:`,
|
|
2697
|
+
error
|
|
2460
2698
|
);
|
|
2461
|
-
await this.integrationManager.registerIntegration(tiktokIntegration);
|
|
2462
2699
|
}
|
|
2463
|
-
}
|
|
2464
|
-
|
|
2465
|
-
`Failed to register ${integrationConfig.type} integration:`,
|
|
2466
|
-
error
|
|
2467
|
-
);
|
|
2468
|
-
}
|
|
2469
|
-
}
|
|
2700
|
+
})
|
|
2701
|
+
);
|
|
2470
2702
|
}
|
|
2471
2703
|
track(actionOrPayload, propertiesOrNothing, contextOrNothing) {
|
|
2472
2704
|
if (!this.config.projectId) {
|
|
@@ -2523,11 +2755,23 @@ var Saasco = class {
|
|
|
2523
2755
|
});
|
|
2524
2756
|
}
|
|
2525
2757
|
this.integrationManager.track(action, properties);
|
|
2526
|
-
|
|
2758
|
+
const eventId = properties?.["event_id"];
|
|
2759
|
+
if (!isBrowser5 || typeof eventId !== "string" || !eventId.trim()) {
|
|
2760
|
+
return this.doRequest("events", requestData);
|
|
2761
|
+
}
|
|
2762
|
+
return deliverOnce(
|
|
2763
|
+
`saasco:${this.config.projectId}:ingest:${action}:${eventId}`,
|
|
2764
|
+
async () => {
|
|
2765
|
+
const result = await this.doRequest("events", requestData);
|
|
2766
|
+
if (!result.success) {
|
|
2767
|
+
throw new Error(result.message);
|
|
2768
|
+
}
|
|
2769
|
+
}
|
|
2770
|
+
).then(() => ({ success: true, message: "Event dispatched" })).catch((error) => ({ success: false, message: String(error) }));
|
|
2527
2771
|
}
|
|
2528
2772
|
/**
|
|
2529
2773
|
* The page method lets you record page views on your website
|
|
2530
|
-
* This records the page title and path and names the event
|
|
2774
|
+
* This records the page title and path and names the event using the reserved name "Page View"
|
|
2531
2775
|
*
|
|
2532
2776
|
* Before implementing this make sure you have disabled the autoPageTracking in the config or you will get duplicate page views
|
|
2533
2777
|
*/
|
|
@@ -2623,7 +2867,10 @@ var Saasco = class {
|
|
|
2623
2867
|
},
|
|
2624
2868
|
context: context || {}
|
|
2625
2869
|
};
|
|
2626
|
-
this.logger.info("identify",
|
|
2870
|
+
this.logger.info("identify", {
|
|
2871
|
+
distinctId,
|
|
2872
|
+
projectId: this.config.projectId
|
|
2873
|
+
});
|
|
2627
2874
|
if (!this.config.enabled) {
|
|
2628
2875
|
return Promise.resolve({
|
|
2629
2876
|
success: true,
|
|
@@ -2659,6 +2906,13 @@ var Saasco = class {
|
|
|
2659
2906
|
message: "Analytics is disabled"
|
|
2660
2907
|
};
|
|
2661
2908
|
}
|
|
2909
|
+
const projectId = requestData.projectId ?? null;
|
|
2910
|
+
const fail = (input) => reportIngestFailure({
|
|
2911
|
+
failure: formatAnalyticsRequestFailure({ path, projectId, ...input }),
|
|
2912
|
+
logger: this.logger,
|
|
2913
|
+
path,
|
|
2914
|
+
seen: this.ingestFailureKeys
|
|
2915
|
+
});
|
|
2662
2916
|
let response;
|
|
2663
2917
|
try {
|
|
2664
2918
|
response = await fetch(url, {
|
|
@@ -2668,46 +2922,22 @@ var Saasco = class {
|
|
|
2668
2922
|
},
|
|
2669
2923
|
body: JSON.stringify(requestData)
|
|
2670
2924
|
});
|
|
2671
|
-
} catch
|
|
2672
|
-
|
|
2673
|
-
this.logger.warn(
|
|
2674
|
-
`Network request failed`,
|
|
2675
|
-
`
|
|
2676
|
-
Error Message: "${message}"`,
|
|
2677
|
-
`
|
|
2678
|
-
Url: "${url}"`
|
|
2679
|
-
);
|
|
2680
|
-
return {
|
|
2681
|
-
success: false,
|
|
2682
|
-
message
|
|
2683
|
-
};
|
|
2925
|
+
} catch {
|
|
2926
|
+
return fail({ reason: "unreachable" });
|
|
2684
2927
|
}
|
|
2928
|
+
let responseBody;
|
|
2685
2929
|
try {
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
}
|
|
2692
|
-
return {
|
|
2693
|
-
success: true,
|
|
2694
|
-
message: responseBody.message ?? ""
|
|
2695
|
-
};
|
|
2696
|
-
} catch (error) {
|
|
2697
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
2698
|
-
this.logger.error(
|
|
2699
|
-
`
|
|
2700
|
-
Error Message: "${message}"`,
|
|
2701
|
-
`
|
|
2702
|
-
Path: "/${path}"`,
|
|
2703
|
-
`
|
|
2704
|
-
Request Data: ${JSON.stringify(requestData, null, 2)}`
|
|
2705
|
-
);
|
|
2706
|
-
return {
|
|
2707
|
-
success: false,
|
|
2708
|
-
message
|
|
2709
|
-
};
|
|
2930
|
+
responseBody = await response.json();
|
|
2931
|
+
} catch {
|
|
2932
|
+
return fail({ reason: "http" });
|
|
2933
|
+
}
|
|
2934
|
+
if (!response.ok) {
|
|
2935
|
+
return fail({ reason: "http", body: responseBody });
|
|
2710
2936
|
}
|
|
2937
|
+
return {
|
|
2938
|
+
success: true,
|
|
2939
|
+
message: responseBody && typeof responseBody === "object" && "message" in responseBody && typeof responseBody.message === "string" ? responseBody.message : ""
|
|
2940
|
+
};
|
|
2711
2941
|
}
|
|
2712
2942
|
/**
|
|
2713
2943
|
* If autoPageTracking is enabled, this will automatically track page views
|
|
@@ -2751,51 +2981,25 @@ Request Data: ${JSON.stringify(requestData, null, 2)}`
|
|
|
2751
2981
|
return this.integrationManager.getStats();
|
|
2752
2982
|
}
|
|
2753
2983
|
/**
|
|
2754
|
-
* Lazily injects the support **loader**
|
|
2755
|
-
*
|
|
2756
|
-
* config as `data-*` attributes. The loader (no React) injects the
|
|
2757
|
-
* cross-origin embed iframe. Deferred (`async`) and guarded against
|
|
2758
|
-
* double-injection so re-running `init()` is a no-op.
|
|
2984
|
+
* Lazily injects the support **loader** (server-gated). See
|
|
2985
|
+
* {@link injectSupportLoader}. Replays identity once the script loads.
|
|
2759
2986
|
*/
|
|
2760
|
-
injectSupport() {
|
|
2761
|
-
if (!isBrowser5
|
|
2762
|
-
return;
|
|
2763
|
-
}
|
|
2764
|
-
const chat = this.config.support;
|
|
2765
|
-
if (!chat) {
|
|
2766
|
-
return;
|
|
2767
|
-
}
|
|
2768
|
-
if (window.__saascoSupportInjected || document.querySelector('script[src*="saasco-support-loader.js"]')) {
|
|
2769
|
-
return;
|
|
2770
|
-
}
|
|
2771
|
-
const scriptUrl = resolveLoaderScriptUrl(
|
|
2772
|
-
"saasco-support-loader.js",
|
|
2773
|
-
chat.scriptUrl
|
|
2774
|
-
);
|
|
2775
|
-
if (!scriptUrl) {
|
|
2776
|
-
this.logger.warn(
|
|
2777
|
-
"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)."
|
|
2778
|
-
);
|
|
2987
|
+
async injectSupport() {
|
|
2988
|
+
if (!isBrowser5) {
|
|
2779
2989
|
return;
|
|
2780
2990
|
}
|
|
2781
|
-
|
|
2782
|
-
const
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
script.setAttribute(
|
|
2790
|
-
attr,
|
|
2791
|
-
attr === "data-base-url" ? normalizeSaascoBaseUrl(value) : value
|
|
2792
|
-
);
|
|
2793
|
-
}
|
|
2794
|
-
}
|
|
2795
|
-
script.addEventListener("load", () => {
|
|
2796
|
-
this.pushSupportIdentity();
|
|
2991
|
+
const { support } = this.config;
|
|
2992
|
+
const scriptUrl = resolveLoaderScriptUrl("saasco-support-loader.js", support?.scriptUrl) ?? `${SAASCO_ORIGIN}/sdk/saasco-support-loader.js`;
|
|
2993
|
+
const result = await injectSupportLoader({
|
|
2994
|
+
baseUrl: support?.baseUrl,
|
|
2995
|
+
onLoad: () => this.pushSupportIdentity(),
|
|
2996
|
+
placeholder: support?.placeholder,
|
|
2997
|
+
projectId: this.config.projectId,
|
|
2998
|
+
scriptUrl
|
|
2797
2999
|
});
|
|
2798
|
-
(
|
|
3000
|
+
if (result === "present") {
|
|
3001
|
+
this.pushSupportIdentity();
|
|
3002
|
+
}
|
|
2799
3003
|
}
|
|
2800
3004
|
/**
|
|
2801
3005
|
* Lazily injects the social-proof loader (server-gated). See
|
|
@@ -2818,27 +3022,23 @@ Request Data: ${JSON.stringify(requestData, null, 2)}`
|
|
|
2818
3022
|
}
|
|
2819
3023
|
/**
|
|
2820
3024
|
* Records the latest CRM identity and forwards it to the support widget.
|
|
2821
|
-
* No-op when support isn't enabled.
|
|
2822
3025
|
*/
|
|
2823
3026
|
updateSupportIdentity(identity) {
|
|
2824
|
-
if (!isBrowser5
|
|
3027
|
+
if (!isBrowser5) {
|
|
2825
3028
|
return;
|
|
2826
3029
|
}
|
|
2827
3030
|
const hasTraits = identity.traits && Object.keys(identity.traits).length > 0;
|
|
2828
3031
|
this.lastSupportIdentity = identity.distinctId || identity.email || hasTraits ? identity : null;
|
|
2829
|
-
if (this.config.support.enabled === false) {
|
|
2830
|
-
return;
|
|
2831
|
-
}
|
|
2832
3032
|
this.pushSupportIdentity();
|
|
2833
3033
|
}
|
|
2834
3034
|
/**
|
|
2835
3035
|
* Pushes the current identity onto `window.SaascoSupport.identify`. The
|
|
2836
3036
|
* widget bundle publishes that global asynchronously, so this short-polls for
|
|
2837
|
-
* it (same approach as tool registration)
|
|
2838
|
-
*
|
|
3037
|
+
* it (same approach as tool registration). `injectSupport` calls this on
|
|
3038
|
+
* script load (or immediately when the loader is already present).
|
|
2839
3039
|
*/
|
|
2840
3040
|
pushSupportIdentity() {
|
|
2841
|
-
if (!isBrowser5
|
|
3041
|
+
if (!isBrowser5) {
|
|
2842
3042
|
return;
|
|
2843
3043
|
}
|
|
2844
3044
|
const identity = this.lastSupportIdentity ?? {
|
|
@@ -2965,31 +3165,31 @@ function getLoggerLevel(config) {
|
|
|
2965
3165
|
}
|
|
2966
3166
|
|
|
2967
3167
|
// ../shared/src/lib/types/browserContext.ts
|
|
2968
|
-
import { z } from "zod";
|
|
2969
|
-
var BrowserContextSchema =
|
|
2970
|
-
$href:
|
|
2971
|
-
$locale:
|
|
2972
|
-
$location:
|
|
2973
|
-
$pathname:
|
|
2974
|
-
$referrer:
|
|
2975
|
-
$referringDomain:
|
|
2976
|
-
$screenDPI:
|
|
2977
|
-
$screenHeight:
|
|
2978
|
-
$screenWidth:
|
|
2979
|
-
$title:
|
|
2980
|
-
$userAgent:
|
|
2981
|
-
$utmAdId:
|
|
2982
|
-
$utmAdSource:
|
|
2983
|
-
$utmCampaign:
|
|
2984
|
-
$utmCampaignId:
|
|
2985
|
-
$utmContent:
|
|
2986
|
-
$utmCreativeFormat:
|
|
2987
|
-
$utmId:
|
|
2988
|
-
$utmMarketingTactic:
|
|
2989
|
-
$utmMedium:
|
|
2990
|
-
$utmSource:
|
|
2991
|
-
$utmSourcePlatform:
|
|
2992
|
-
$utmTerm:
|
|
3168
|
+
import { z as z2 } from "zod";
|
|
3169
|
+
var BrowserContextSchema = z2.object({
|
|
3170
|
+
$href: z2.string(),
|
|
3171
|
+
$locale: z2.string(),
|
|
3172
|
+
$location: z2.string(),
|
|
3173
|
+
$pathname: z2.string(),
|
|
3174
|
+
$referrer: z2.string(),
|
|
3175
|
+
$referringDomain: z2.string(),
|
|
3176
|
+
$screenDPI: z2.number(),
|
|
3177
|
+
$screenHeight: z2.number(),
|
|
3178
|
+
$screenWidth: z2.number(),
|
|
3179
|
+
$title: z2.string(),
|
|
3180
|
+
$userAgent: z2.string(),
|
|
3181
|
+
$utmAdId: z2.string().nullable(),
|
|
3182
|
+
$utmAdSource: z2.string().nullable(),
|
|
3183
|
+
$utmCampaign: z2.string().nullable(),
|
|
3184
|
+
$utmCampaignId: z2.string().nullable(),
|
|
3185
|
+
$utmContent: z2.string().nullable(),
|
|
3186
|
+
$utmCreativeFormat: z2.string().nullable(),
|
|
3187
|
+
$utmId: z2.string().nullable(),
|
|
3188
|
+
$utmMarketingTactic: z2.string().nullable(),
|
|
3189
|
+
$utmMedium: z2.string().nullable(),
|
|
3190
|
+
$utmSource: z2.string().nullable(),
|
|
3191
|
+
$utmSourcePlatform: z2.string().nullable(),
|
|
3192
|
+
$utmTerm: z2.string().nullable()
|
|
2993
3193
|
});
|
|
2994
3194
|
export {
|
|
2995
3195
|
BrowserContextSchema,
|