@faststats/web 0.6.0 → 0.8.0
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 +67 -0
- package/dist/chunks/api-urls-DWk43fu6.js +53 -0
- package/dist/chunks/extensions-DW7tJY0T.d.ts +51 -0
- package/dist/chunks/identifiers-KgQDX74Q.js +21 -0
- package/dist/chunks/send-data-DFZnsaL2.js +219 -0
- package/dist/error.d.ts +9 -4
- package/dist/error.js +89 -59
- package/dist/index.d.ts +54 -72
- package/dist/index.js +349 -353
- package/dist/outbound-links.d.ts +10 -0
- package/dist/outbound-links.js +62 -0
- package/dist/replay.d.ts +128 -2
- package/dist/replay.js +154 -62
- package/dist/web-vitals.d.ts +10 -5
- package/dist/web-vitals.js +56 -50
- package/package.json +11 -12
- package/dist/chunks/api-urls-CaNa8upY.js +0 -45
- package/dist/chunks/identifiers-cUymVK5H.js +0 -20
- package/dist/chunks/replay-DgodPHzA.d.ts +0 -104
- package/dist/chunks/send-data-DjdbGDDc.js +0 -196
- package/dist/feature-flags.d.ts +0 -18
- package/dist/feature-flags.js +0 -32
package/dist/index.js
CHANGED
|
@@ -1,224 +1,291 @@
|
|
|
1
|
-
import { a as
|
|
2
|
-
import {
|
|
3
|
-
import { n as resetAnonymousId, t as getAnonymousId } from "./chunks/identifiers-
|
|
4
|
-
//#region src/
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
1
|
+
import { a as sanitizeUrl, i as resolveBaseUrl, n as URLS, r as getPageContext, t as ANALYTICS_BASE } from "./chunks/api-urls-DWk43fu6.js";
|
|
2
|
+
import { a as resetSession, o as touchActivity, r as getSessionContext, t as sendData } from "./chunks/send-data-DFZnsaL2.js";
|
|
3
|
+
import { n as resetAnonymousId, t as getAnonymousId } from "./chunks/identifiers-KgQDX74Q.js";
|
|
4
|
+
//#region src/extensions.ts
|
|
5
|
+
var ExtensionHost = class {
|
|
6
|
+
runtime;
|
|
7
|
+
handlers = /* @__PURE__ */ new Map();
|
|
8
|
+
cleanups = [];
|
|
9
|
+
stopped = false;
|
|
10
|
+
constructor(extensions, runtime) {
|
|
11
|
+
this.runtime = runtime;
|
|
12
|
+
for (const extension of extensions) this.setup(extension);
|
|
13
|
+
}
|
|
14
|
+
emit(hook, event) {
|
|
15
|
+
if (this.stopped) return;
|
|
16
|
+
for (const handler of this.handlers.get(hook) ?? []) try {
|
|
17
|
+
handler(event)?.catch((error) => this.runtime.log(`${hook} failed`, error));
|
|
18
|
+
} catch (error) {
|
|
19
|
+
this.runtime.log(`${hook} failed`, error);
|
|
20
|
+
}
|
|
13
21
|
}
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
this.
|
|
24
|
-
|
|
22
|
+
stop(discard = false) {
|
|
23
|
+
if (this.stopped) return;
|
|
24
|
+
this.stopped = true;
|
|
25
|
+
for (const cleanup of this.cleanups.reverse()) try {
|
|
26
|
+
cleanup({ discard });
|
|
27
|
+
} catch (error) {
|
|
28
|
+
this.runtime.log("cleanup failed", error);
|
|
29
|
+
}
|
|
30
|
+
this.cleanups.length = 0;
|
|
31
|
+
this.handlers.clear();
|
|
32
|
+
}
|
|
33
|
+
async setup(extension) {
|
|
34
|
+
if (this.stopped) return;
|
|
35
|
+
const subscriptions = [];
|
|
36
|
+
try {
|
|
37
|
+
const cleanup = await extension.setup({
|
|
38
|
+
...this.runtime,
|
|
39
|
+
on: (hook, handler) => {
|
|
40
|
+
if (this.stopped) return () => {};
|
|
41
|
+
let handlers = this.handlers.get(hook);
|
|
42
|
+
if (!handlers) {
|
|
43
|
+
handlers = /* @__PURE__ */ new Set();
|
|
44
|
+
this.handlers.set(hook, handlers);
|
|
45
|
+
}
|
|
46
|
+
handlers.add(handler);
|
|
47
|
+
const off = () => handlers.delete(handler);
|
|
48
|
+
subscriptions.push(off);
|
|
49
|
+
return off;
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
if (this.stopped) cleanup?.({ discard: true });
|
|
53
|
+
else if (cleanup) this.cleanups.push(cleanup);
|
|
54
|
+
} catch (error) {
|
|
55
|
+
for (const off of subscriptions) off();
|
|
56
|
+
this.runtime.log(`${extension.name} setup failed`, error);
|
|
57
|
+
}
|
|
25
58
|
}
|
|
26
59
|
};
|
|
27
|
-
function isEligibleActivation(event) {
|
|
28
|
-
if (event.defaultPrevented) return false;
|
|
29
|
-
if (event.type === "click") return event.button === 0;
|
|
30
|
-
if (event.type === "auxclick") return event.button === 1;
|
|
31
|
-
return false;
|
|
32
|
-
}
|
|
33
|
-
function findOutboundAnchor(event) {
|
|
34
|
-
for (const node of event.composedPath()) if (isOutboundAnchor(node)) return node;
|
|
35
|
-
let node = event.target;
|
|
36
|
-
while (node) {
|
|
37
|
-
if (isOutboundAnchor(node)) return node;
|
|
38
|
-
node = node.parentNode;
|
|
39
|
-
}
|
|
40
|
-
return null;
|
|
41
|
-
}
|
|
42
|
-
function isOutboundAnchor(node) {
|
|
43
|
-
return node instanceof HTMLAnchorElement && !!node.href && node.host !== location.host;
|
|
44
|
-
}
|
|
45
60
|
//#endregion
|
|
46
61
|
//#region src/analytics.ts
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
function
|
|
50
|
-
return
|
|
51
|
-
|
|
52
|
-
function
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
if (instance) instance.setConsentMode(mode);
|
|
67
|
-
else pendingConsentMode = mode;
|
|
68
|
-
}
|
|
69
|
-
function optIn() {
|
|
70
|
-
setConsentMode("granted");
|
|
71
|
-
}
|
|
72
|
-
function optOut() {
|
|
73
|
-
setConsentMode("denied");
|
|
74
|
-
}
|
|
75
|
-
function reportError(error) {
|
|
76
|
-
activeInstance()?.reportError(error);
|
|
62
|
+
const navigationListeners = /* @__PURE__ */ new Set();
|
|
63
|
+
const historyPatches = {};
|
|
64
|
+
function patchHistory(method) {
|
|
65
|
+
if (historyPatches[method]?.owner === history) return;
|
|
66
|
+
const original = history[method];
|
|
67
|
+
const wrapped = function(data, unused, url) {
|
|
68
|
+
Reflect.apply(original, this, [
|
|
69
|
+
data,
|
|
70
|
+
unused,
|
|
71
|
+
url
|
|
72
|
+
]);
|
|
73
|
+
for (const listener of navigationListeners) listener();
|
|
74
|
+
};
|
|
75
|
+
history[method] = wrapped;
|
|
76
|
+
historyPatches[method] = {
|
|
77
|
+
owner: history,
|
|
78
|
+
original,
|
|
79
|
+
wrapped
|
|
80
|
+
};
|
|
77
81
|
}
|
|
78
|
-
function
|
|
79
|
-
|
|
80
|
-
|
|
82
|
+
function observeHistory(listener) {
|
|
83
|
+
navigationListeners.add(listener);
|
|
84
|
+
patchHistory("pushState");
|
|
85
|
+
patchHistory("replaceState");
|
|
86
|
+
return () => {
|
|
87
|
+
navigationListeners.delete(listener);
|
|
88
|
+
if (navigationListeners.size) return;
|
|
89
|
+
for (const method of ["pushState", "replaceState"]) {
|
|
90
|
+
const patch = historyPatches[method];
|
|
91
|
+
if (patch && history[method] === patch.wrapped) {
|
|
92
|
+
history[method] = patch.original;
|
|
93
|
+
delete historyPatches[method];
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
};
|
|
81
97
|
}
|
|
82
98
|
var WebAnalytics = class {
|
|
83
99
|
options;
|
|
84
100
|
baseUrl;
|
|
85
101
|
debug;
|
|
102
|
+
cleanup = [];
|
|
86
103
|
started = false;
|
|
87
|
-
destroyed = false;
|
|
88
|
-
epoch = 0;
|
|
89
104
|
heartbeatTimer = null;
|
|
105
|
+
extensions = null;
|
|
90
106
|
pageKey = "";
|
|
91
107
|
entry = {
|
|
92
108
|
path: "",
|
|
93
109
|
url: "",
|
|
94
|
-
|
|
110
|
+
search: ""
|
|
95
111
|
};
|
|
96
112
|
left = false;
|
|
97
113
|
visitDuration = 0;
|
|
98
114
|
visitStartedAt = null;
|
|
99
115
|
pendingPageviewTrigger = null;
|
|
100
116
|
consentMode;
|
|
101
|
-
pendingBehavior;
|
|
102
|
-
cleanup = [];
|
|
103
|
-
childTrackers = [];
|
|
104
|
-
outboundLinks = new OutboundLinkTracker();
|
|
105
117
|
constructor(options) {
|
|
106
118
|
this.options = options;
|
|
119
|
+
if (!options.siteKey.trim()) throw new Error("siteKey is required");
|
|
107
120
|
this.baseUrl = resolveBaseUrl(options.baseUrl, ANALYTICS_BASE);
|
|
108
121
|
this.debug = options.debug ?? false;
|
|
109
|
-
this.consentMode =
|
|
110
|
-
pendingConsentMode = void 0;
|
|
111
|
-
this.pendingBehavior = options.consent?.pendingBehavior ?? "anonymous";
|
|
112
|
-
if (options.autoTrack ?? true) {
|
|
113
|
-
if (typeof window === "undefined") return;
|
|
114
|
-
if (isTrackingDisabled()) {
|
|
115
|
-
this.log("disabled");
|
|
116
|
-
return;
|
|
117
|
-
}
|
|
118
|
-
setTimeout(() => void this.start(), 0);
|
|
119
|
-
}
|
|
122
|
+
this.consentMode = options.consent ?? "granted";
|
|
120
123
|
}
|
|
121
|
-
|
|
122
|
-
if (this.
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
124
|
+
start() {
|
|
125
|
+
if (this.started !== false || typeof window === "undefined") return;
|
|
126
|
+
this.started = true;
|
|
127
|
+
this.installBrowserLifecycle();
|
|
128
|
+
if (this.canTrack()) this.activate("load");
|
|
126
129
|
}
|
|
127
|
-
|
|
128
|
-
if (this.
|
|
129
|
-
if (
|
|
130
|
-
|
|
130
|
+
destroy() {
|
|
131
|
+
if (this.started === null) return;
|
|
132
|
+
if (this.started && typeof window !== "undefined" && this.canTrack()) this.leavePage();
|
|
133
|
+
this.started = null;
|
|
134
|
+
this.pendingPageviewTrigger = null;
|
|
135
|
+
this.stopHeartbeat();
|
|
136
|
+
for (const cleanup of this.cleanup.splice(0).reverse()) cleanup();
|
|
137
|
+
this.stopExtensions(false);
|
|
131
138
|
}
|
|
132
|
-
|
|
133
|
-
|
|
139
|
+
pageview(properties = {}) {
|
|
140
|
+
if (!this.ensureActive()) return;
|
|
141
|
+
const includeHash = this.options.trackHash ?? false;
|
|
142
|
+
const key = `${location.pathname}|${location.search}|${includeHash ? location.hash : ""}`;
|
|
143
|
+
if (key === this.pageKey) return;
|
|
144
|
+
this.pageKey = key;
|
|
145
|
+
this.send("pageview", properties);
|
|
134
146
|
}
|
|
135
|
-
|
|
136
|
-
|
|
147
|
+
track(name, properties = {}) {
|
|
148
|
+
if (!this.ensureActive()) return;
|
|
149
|
+
const eventName = name.trim();
|
|
150
|
+
if (!eventName || eventName === "pageview" || eventName === "page_leave" || eventName === "error" || eventName === "outbound_link") return;
|
|
151
|
+
this.send(eventName, properties);
|
|
152
|
+
}
|
|
153
|
+
async identify(userOrExternalId, email, options = {}) {
|
|
154
|
+
if (!this.ensureActive() || !this.canTrack() || this.isCookieless()) return false;
|
|
155
|
+
const user = typeof userOrExternalId === "string" ? {
|
|
156
|
+
id: userOrExternalId,
|
|
157
|
+
email,
|
|
158
|
+
...options
|
|
159
|
+
} : userOrExternalId;
|
|
160
|
+
const externalId = user.id.trim();
|
|
161
|
+
if (!externalId) return false;
|
|
162
|
+
const clearFields = [
|
|
163
|
+
"email",
|
|
164
|
+
"name",
|
|
165
|
+
"phone",
|
|
166
|
+
"avatarUrl"
|
|
167
|
+
].filter((field) => user[field] === null);
|
|
168
|
+
return await sendData({
|
|
169
|
+
url: `${this.baseUrl}${URLS.identify}`,
|
|
170
|
+
data: JSON.stringify({
|
|
171
|
+
token: this.options.siteKey,
|
|
172
|
+
identifier: this.getAnonymousId(),
|
|
173
|
+
externalId,
|
|
174
|
+
email: user.email?.trim() || void 0,
|
|
175
|
+
name: user.name?.trim() || void 0,
|
|
176
|
+
phone: user.phone?.trim() || void 0,
|
|
177
|
+
avatarUrl: user.avatarUrl?.trim() || void 0,
|
|
178
|
+
traits: user.traits,
|
|
179
|
+
replaceTraits: user.traitMode === "replace",
|
|
180
|
+
unsetTraits: user.unsetTraits,
|
|
181
|
+
aliases: user.aliases,
|
|
182
|
+
clearFields
|
|
183
|
+
}),
|
|
184
|
+
contentType: "text/plain",
|
|
185
|
+
debug: this.debug,
|
|
186
|
+
debugPrefix: "[FastStats] identify",
|
|
187
|
+
useBeacon: false
|
|
188
|
+
});
|
|
137
189
|
}
|
|
138
|
-
|
|
139
|
-
|
|
190
|
+
logout(resetAnonymousIdentity = true) {
|
|
191
|
+
if (!this.ensureActive()) return;
|
|
192
|
+
if (resetAnonymousIdentity) resetAnonymousId(this.options.siteKey, this.isCookieless());
|
|
193
|
+
resetSession(this.options.siteKey, this.isCookieless());
|
|
140
194
|
}
|
|
141
|
-
|
|
142
|
-
if (
|
|
195
|
+
setConsentMode(mode) {
|
|
196
|
+
if (mode === this.consentMode) return;
|
|
197
|
+
const wasTracking = this.canTrack();
|
|
198
|
+
const wasCookieless = this.isCookieless();
|
|
199
|
+
const willTrack = mode !== "denied";
|
|
200
|
+
const willBeCookieless = this.options.cookieless === true || mode === "anonymous";
|
|
201
|
+
if (wasTracking && willTrack && wasCookieless !== willBeCookieless) this.leavePage();
|
|
202
|
+
this.consentMode = mode;
|
|
203
|
+
if (!willTrack) {
|
|
204
|
+
this.pauseVisit();
|
|
205
|
+
this.stopHeartbeat();
|
|
206
|
+
this.pendingPageviewTrigger = null;
|
|
207
|
+
this.stopExtensions(true);
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
if (wasTracking) {
|
|
211
|
+
this.extensions?.emit("consentChange", { cookieless: willBeCookieless });
|
|
212
|
+
if (wasCookieless !== willBeCookieless) {
|
|
213
|
+
this.enterPage();
|
|
214
|
+
this.pageKey = "";
|
|
215
|
+
this.pageview({ trigger: "consent" });
|
|
216
|
+
}
|
|
217
|
+
} else if (this.started) this.activate("consent");
|
|
143
218
|
}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
this.cleanup.push(() => target.removeEventListener(type, listener, options));
|
|
219
|
+
getAnonymousId() {
|
|
220
|
+
return this.canTrack() ? getAnonymousId(this.options.siteKey, this.isCookieless()) : "";
|
|
147
221
|
}
|
|
148
|
-
|
|
149
|
-
if (!this.
|
|
150
|
-
|
|
151
|
-
this.heartbeatTimer = null;
|
|
222
|
+
reportError(error) {
|
|
223
|
+
if (!this.ensureActive() || !this.canTrack()) return;
|
|
224
|
+
this.extensions?.emit("error", { error });
|
|
152
225
|
}
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
create().then(async (tracker) => {
|
|
156
|
-
if (this.epoch !== epoch || !this.started) return tracker.stop?.();
|
|
157
|
-
await tracker.start();
|
|
158
|
-
if (this.epoch !== epoch || !this.started) return tracker.stop?.();
|
|
159
|
-
this.childTrackers.push(tracker);
|
|
160
|
-
this.log(`${name} loaded`);
|
|
161
|
-
}).catch((error) => this.log(`failed to initialize ${name} tracker: ${String(error)}`));
|
|
226
|
+
canTrack() {
|
|
227
|
+
return this.consentMode !== "denied";
|
|
162
228
|
}
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
const base = {
|
|
166
|
-
siteKey: o.siteKey,
|
|
167
|
-
baseUrl: this.baseUrl,
|
|
168
|
-
debug: this.debug
|
|
169
|
-
};
|
|
170
|
-
if (o.sessionReplays?.enabled) this.loadChild("replay", async () => {
|
|
171
|
-
const { default: ReplayTracker } = await import("./replay.js");
|
|
172
|
-
return new ReplayTracker({
|
|
173
|
-
...base,
|
|
174
|
-
...o.replayOptions
|
|
175
|
-
});
|
|
176
|
-
});
|
|
177
|
-
if (o.errorTracking?.enabled) this.loadChild("error", async () => {
|
|
178
|
-
const { default: ErrorTracker } = await import("./error.js");
|
|
179
|
-
return new ErrorTracker({
|
|
180
|
-
...base,
|
|
181
|
-
sdkName: o.sdkName,
|
|
182
|
-
sdkVersion: o.sdkVersion
|
|
183
|
-
});
|
|
184
|
-
});
|
|
185
|
-
if (o.webVitals?.enabled) this.loadChild("web-vitals", async () => {
|
|
186
|
-
const { default: WebVitalsTracker } = await import("./web-vitals.js");
|
|
187
|
-
return new WebVitalsTracker({
|
|
188
|
-
...base,
|
|
189
|
-
attribution: o.webVitals?.attribution ?? false
|
|
190
|
-
});
|
|
191
|
-
});
|
|
229
|
+
isCookieless() {
|
|
230
|
+
return this.options.cookieless === true || this.consentMode === "anonymous";
|
|
192
231
|
}
|
|
193
|
-
|
|
194
|
-
this.
|
|
195
|
-
path: location.pathname,
|
|
196
|
-
url: sanitizeUrl(location.href),
|
|
197
|
-
hash: location.hash
|
|
198
|
-
};
|
|
199
|
-
this.left = false;
|
|
200
|
-
this.startVisit();
|
|
232
|
+
ensureActive() {
|
|
233
|
+
return this.started === true && typeof window !== "undefined";
|
|
201
234
|
}
|
|
202
|
-
|
|
235
|
+
activate(trigger) {
|
|
203
236
|
this.touch();
|
|
204
|
-
this.
|
|
237
|
+
this.startExtensions();
|
|
205
238
|
this.enterPage();
|
|
206
239
|
this.pageKey = "";
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
}
|
|
211
|
-
this.pendingPageviewTrigger = trigger;
|
|
240
|
+
this.startHeartbeat();
|
|
241
|
+
if (document.visibilityState === "visible") this.pageview({ trigger });
|
|
242
|
+
else this.pendingPageviewTrigger = trigger;
|
|
212
243
|
}
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
244
|
+
startExtensions() {
|
|
245
|
+
if (this.extensions || !this.options.extensions?.length) return;
|
|
246
|
+
const host = new ExtensionHost(this.options.extensions, {
|
|
247
|
+
siteKey: this.options.siteKey,
|
|
248
|
+
baseUrl: this.baseUrl,
|
|
249
|
+
debug: this.debug,
|
|
250
|
+
trackHash: this.options.trackHash ?? false,
|
|
251
|
+
sdkName: this.options.sdkName,
|
|
252
|
+
sdkVersion: this.options.sdkVersion,
|
|
253
|
+
isCookieless: () => this.isCookieless(),
|
|
254
|
+
getAnonymousId: () => this.getAnonymousId(),
|
|
255
|
+
getSession: () => getSessionContext(this.options.siteKey, this.isCookieless()),
|
|
256
|
+
capture: (event, properties) => this.send(event, properties),
|
|
257
|
+
log: (...details) => {
|
|
258
|
+
if (this.debug) console.log(...details);
|
|
259
|
+
}
|
|
260
|
+
});
|
|
261
|
+
this.extensions = host;
|
|
262
|
+
}
|
|
263
|
+
stopExtensions(discard) {
|
|
264
|
+
this.extensions?.stop(discard);
|
|
265
|
+
this.extensions = null;
|
|
266
|
+
}
|
|
267
|
+
installBrowserLifecycle() {
|
|
268
|
+
this.bind(document, "visibilitychange", this.onVisibilityChange);
|
|
269
|
+
this.bind(window, "pagehide", this.onPageHide);
|
|
270
|
+
this.bind(window, "pageshow", this.onPageShow);
|
|
271
|
+
this.bind(window, "popstate", this.onNavigate);
|
|
272
|
+
if (this.options.trackHash) this.bind(window, "hashchange", this.onNavigate);
|
|
273
|
+
this.cleanup.push(observeHistory(() => this.navigate()));
|
|
274
|
+
}
|
|
275
|
+
bind(target, type, listener, options) {
|
|
276
|
+
target.addEventListener(type, listener, options);
|
|
277
|
+
this.cleanup.push(() => target.removeEventListener(type, listener, options));
|
|
278
|
+
}
|
|
279
|
+
onNavigate = () => this.navigate();
|
|
217
280
|
onVisibilityChange = () => {
|
|
281
|
+
if (!this.canTrack()) return;
|
|
218
282
|
this.touch();
|
|
219
283
|
if (document.visibilityState === "hidden") {
|
|
220
284
|
this.pauseVisit();
|
|
221
|
-
|
|
285
|
+
this.extensions?.emit("pageHide", {
|
|
286
|
+
persisted: false,
|
|
287
|
+
terminal: false
|
|
288
|
+
});
|
|
222
289
|
this.stopHeartbeat();
|
|
223
290
|
return;
|
|
224
291
|
}
|
|
@@ -229,216 +296,145 @@ var WebAnalytics = class {
|
|
|
229
296
|
if (trigger) this.pageview({ trigger });
|
|
230
297
|
};
|
|
231
298
|
onPageHide = (event) => {
|
|
299
|
+
if (!this.canTrack()) return;
|
|
232
300
|
const persisted = event.persisted;
|
|
233
301
|
this.pauseVisit();
|
|
234
|
-
|
|
302
|
+
this.extensions?.emit("pageHide", {
|
|
303
|
+
persisted,
|
|
304
|
+
terminal: !persisted
|
|
305
|
+
});
|
|
235
306
|
if (!persisted) this.leavePage();
|
|
236
307
|
};
|
|
237
308
|
onPageShow = (event) => {
|
|
309
|
+
if (!this.canTrack()) return;
|
|
238
310
|
if (document.visibilityState === "visible") this.resumeVisit();
|
|
239
|
-
|
|
311
|
+
this.extensions?.emit("pageShow", { persisted: event.persisted });
|
|
240
312
|
};
|
|
241
|
-
|
|
242
|
-
if (this.started) return;
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
}
|
|
251
|
-
this.started = true;
|
|
252
|
-
instance = this;
|
|
253
|
-
setCookielessMode(this.cookieless());
|
|
254
|
-
this.bind(document, "click", this.onLinkClick, true);
|
|
255
|
-
this.bind(document, "auxclick", this.onLinkClick, true);
|
|
256
|
-
this.bind(document, "visibilitychange", this.onVisibilityChange);
|
|
257
|
-
this.bind(window, "pagehide", this.onPageHide);
|
|
258
|
-
this.bind(window, "pageshow", this.onPageShow);
|
|
259
|
-
this.bind(window, "popstate", () => this.navigate());
|
|
260
|
-
if (this.options.trackHash ?? false) this.bind(window, "hashchange", () => this.navigate());
|
|
261
|
-
for (const method of ["pushState", "replaceState"]) {
|
|
262
|
-
const original = history[method];
|
|
263
|
-
const wrapped = (data, unused, url) => {
|
|
264
|
-
Reflect.apply(original, history, [
|
|
265
|
-
data,
|
|
266
|
-
unused,
|
|
267
|
-
url
|
|
268
|
-
]);
|
|
269
|
-
this.navigate();
|
|
270
|
-
};
|
|
271
|
-
history[method] = wrapped;
|
|
272
|
-
this.cleanup.push(() => {
|
|
273
|
-
if (history[method] === wrapped) history[method] = original;
|
|
274
|
-
});
|
|
275
|
-
}
|
|
276
|
-
this.startHeartbeat();
|
|
277
|
-
if (this.canTrack()) this.beginTracking("load");
|
|
278
|
-
}
|
|
279
|
-
destroy() {
|
|
280
|
-
if (this.destroyed) return;
|
|
281
|
-
this.epoch++;
|
|
282
|
-
if (this.started && typeof window !== "undefined") this.leavePage();
|
|
283
|
-
this.pendingPageviewTrigger = null;
|
|
284
|
-
this.stopHeartbeat();
|
|
285
|
-
for (const fn of this.cleanup.splice(0)) fn();
|
|
286
|
-
for (const tracker of this.childTrackers.splice(0)) tracker.stop?.();
|
|
287
|
-
if (instance === this) instance = null;
|
|
288
|
-
this.started = false;
|
|
289
|
-
this.destroyed = true;
|
|
290
|
-
}
|
|
291
|
-
pageview(extra = {}) {
|
|
292
|
-
if (!this.ensureActive()) return;
|
|
293
|
-
const trackHash = this.options.trackHash ?? false;
|
|
294
|
-
const key = `${location.pathname}|${trackHash ? location.hash : ""}`;
|
|
295
|
-
if (key === this.pageKey) return;
|
|
296
|
-
this.pageKey = key;
|
|
297
|
-
this.send("pageview", extra);
|
|
298
|
-
}
|
|
299
|
-
track(name, extra = {}) {
|
|
300
|
-
if (!this.ensureActive()) return;
|
|
301
|
-
const eventName = name.trim();
|
|
302
|
-
if (!eventName) return;
|
|
303
|
-
this.send(eventName, extra);
|
|
304
|
-
}
|
|
305
|
-
async identify(externalId, email, options = {}) {
|
|
306
|
-
if (!this.ensureActive() || !this.canTrack() || this.cookieless()) return false;
|
|
307
|
-
const trimmedExternalId = externalId.trim();
|
|
308
|
-
const trimmedEmail = email.trim();
|
|
309
|
-
if (!trimmedExternalId || !trimmedEmail) return false;
|
|
310
|
-
return sendData({
|
|
311
|
-
url: `${this.baseUrl}${URLS.identify}`,
|
|
312
|
-
data: JSON.stringify({
|
|
313
|
-
token: this.options.siteKey,
|
|
314
|
-
identifier: this.getAnonymousId(),
|
|
315
|
-
externalId: trimmedExternalId,
|
|
316
|
-
email: trimmedEmail,
|
|
317
|
-
name: options.name?.trim() || void 0,
|
|
318
|
-
phone: options.phone?.trim() || void 0,
|
|
319
|
-
avatarUrl: options.avatarUrl?.trim() || void 0,
|
|
320
|
-
traits: options.traits ?? {}
|
|
321
|
-
}),
|
|
322
|
-
contentType: "text/plain",
|
|
323
|
-
debug: this.debug,
|
|
324
|
-
debugPrefix: "[Analytics] identify",
|
|
325
|
-
useBeacon: false
|
|
326
|
-
});
|
|
327
|
-
}
|
|
328
|
-
logout(resetAnonymousIdentity = true) {
|
|
329
|
-
if (!this.ensureActive()) return;
|
|
330
|
-
if (resetAnonymousIdentity) resetAnonymousId(this.cookieless());
|
|
331
|
-
resetSession(this.options.siteKey);
|
|
332
|
-
}
|
|
333
|
-
setConsentMode(mode) {
|
|
334
|
-
const wasBlocked = !this.canTrack();
|
|
335
|
-
this.consentMode = mode;
|
|
336
|
-
const cookieless = this.cookieless();
|
|
337
|
-
setCookielessMode(cookieless);
|
|
338
|
-
for (const tracker of this.childTrackers) tracker.setCookielessMode?.(cookieless);
|
|
339
|
-
if (wasBlocked && this.canTrack() && this.started) this.beginTracking("consent");
|
|
340
|
-
}
|
|
341
|
-
getConsentMode() {
|
|
342
|
-
return this.consentMode;
|
|
343
|
-
}
|
|
344
|
-
getAnonymousId() {
|
|
345
|
-
if (!this.canTrack()) return "";
|
|
346
|
-
return getAnonymousId(this.cookieless());
|
|
347
|
-
}
|
|
348
|
-
getSessionId() {
|
|
349
|
-
return getSessionContext(this.options.siteKey, this.cookieless()).sessionId;
|
|
350
|
-
}
|
|
351
|
-
getWindowId() {
|
|
352
|
-
return getSessionContext(this.options.siteKey, this.cookieless()).windowId;
|
|
353
|
-
}
|
|
354
|
-
async checkFeatureFlag(key, attributes, opts) {
|
|
355
|
-
if (typeof window === "undefined" || isTrackingDisabled() || !this.canTrack()) return { value: "false" };
|
|
356
|
-
const externalId = opts?.externalId?.trim();
|
|
357
|
-
const identifier = this.getAnonymousId();
|
|
358
|
-
if (!externalId && !identifier) return { value: "false" };
|
|
359
|
-
const { fetchFeatureFlagEvaluation } = await import("./feature-flags.js");
|
|
360
|
-
return fetchFeatureFlagEvaluation(key, {
|
|
361
|
-
baseUrl: resolveBaseUrl(this.options.featureFlagsBaseUrl, FEATURE_FLAGS_BASE),
|
|
362
|
-
projectToken: this.options.siteKey,
|
|
363
|
-
...identifier ? { identifier } : {},
|
|
364
|
-
...externalId ? { externalId } : {},
|
|
365
|
-
sessionId: this.getSessionId(),
|
|
366
|
-
attributes,
|
|
367
|
-
signal: opts?.signal
|
|
368
|
-
});
|
|
369
|
-
}
|
|
370
|
-
reportError(error) {
|
|
371
|
-
if (!this.ensureActive() || !this.options.errorTracking?.enabled || !this.canTrack()) return;
|
|
372
|
-
this.childTrackers.find((tracker) => tracker.captureError)?.captureError?.(error);
|
|
313
|
+
navigate() {
|
|
314
|
+
if (this.started !== true || !this.canTrack()) return;
|
|
315
|
+
const includeHash = this.options.trackHash ?? false;
|
|
316
|
+
if (`${location.pathname}${includeHash ? location.hash : ""}` === this.entry.path && location.search === this.entry.search) return;
|
|
317
|
+
const url = sanitizeUrl(location.href, includeHash);
|
|
318
|
+
this.extensions?.emit("pageChange", { url });
|
|
319
|
+
this.leavePage();
|
|
320
|
+
this.enterPage();
|
|
321
|
+
this.pageview({ trigger: "navigation" });
|
|
373
322
|
}
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
const
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
sessionId: ctx.sessionId,
|
|
386
|
-
windowId: ctx.windowId,
|
|
387
|
-
event,
|
|
388
|
-
...getPageContext(),
|
|
389
|
-
...dims,
|
|
390
|
-
properties
|
|
391
|
-
}),
|
|
392
|
-
contentType: "text/plain",
|
|
393
|
-
debug: this.debug,
|
|
394
|
-
debugPrefix: `[Analytics] ${event}`
|
|
395
|
-
});
|
|
323
|
+
enterPage() {
|
|
324
|
+
const includeHash = this.options.trackHash ?? false;
|
|
325
|
+
const hash = includeHash ? location.hash : "";
|
|
326
|
+
this.entry = {
|
|
327
|
+
path: `${location.pathname}${hash}`,
|
|
328
|
+
url: sanitizeUrl(location.href, includeHash),
|
|
329
|
+
search: location.search
|
|
330
|
+
};
|
|
331
|
+
this.left = false;
|
|
332
|
+
this.visitDuration = 0;
|
|
333
|
+
this.visitStartedAt = document.visibilityState === "visible" ? Date.now() : null;
|
|
396
334
|
}
|
|
397
335
|
leavePage() {
|
|
398
|
-
if (this.
|
|
336
|
+
if (this.started !== true || this.left || !this.canTrack()) return;
|
|
399
337
|
this.pauseVisit();
|
|
400
338
|
this.left = true;
|
|
401
|
-
const
|
|
339
|
+
const session = getSessionContext(this.options.siteKey, this.isCookieless());
|
|
402
340
|
this.send("page_leave", {
|
|
403
341
|
time_on_page: this.visitDuration,
|
|
404
|
-
session_duration: Date.now() -
|
|
342
|
+
session_duration: Date.now() - session.sessionStart
|
|
405
343
|
}, {
|
|
406
344
|
page: this.entry.path,
|
|
407
345
|
url: this.entry.url
|
|
408
346
|
});
|
|
409
347
|
}
|
|
410
|
-
startVisit() {
|
|
411
|
-
this.visitDuration = 0;
|
|
412
|
-
this.visitStartedAt = document.visibilityState === "visible" ? Date.now() : null;
|
|
413
|
-
}
|
|
414
348
|
pauseVisit() {
|
|
415
349
|
if (this.visitStartedAt === null) return;
|
|
416
350
|
this.visitDuration += Math.max(0, Date.now() - this.visitStartedAt);
|
|
417
351
|
this.visitStartedAt = null;
|
|
418
352
|
}
|
|
419
353
|
resumeVisit() {
|
|
420
|
-
if (this.left
|
|
421
|
-
|
|
354
|
+
if (!this.left && this.visitStartedAt === null) this.visitStartedAt = Date.now();
|
|
355
|
+
}
|
|
356
|
+
touch() {
|
|
357
|
+
if (this.canTrack()) touchActivity(this.options.siteKey, this.isCookieless());
|
|
422
358
|
}
|
|
423
359
|
startHeartbeat() {
|
|
424
360
|
this.stopHeartbeat();
|
|
425
361
|
this.heartbeatTimer = setInterval(() => {
|
|
426
|
-
if (document.visibilityState === "hidden")
|
|
427
|
-
this.stopHeartbeat();
|
|
428
|
-
return;
|
|
429
|
-
}
|
|
362
|
+
if (document.visibilityState === "hidden") return this.stopHeartbeat();
|
|
430
363
|
this.touch();
|
|
431
|
-
},
|
|
364
|
+
}, 3e5);
|
|
432
365
|
}
|
|
433
|
-
|
|
434
|
-
if (!this.
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
this.
|
|
440
|
-
this.
|
|
366
|
+
stopHeartbeat() {
|
|
367
|
+
if (!this.heartbeatTimer) return;
|
|
368
|
+
clearInterval(this.heartbeatTimer);
|
|
369
|
+
this.heartbeatTimer = null;
|
|
370
|
+
}
|
|
371
|
+
send(event, properties = {}, dimensions = {}) {
|
|
372
|
+
if (this.started !== true || !this.canTrack()) return;
|
|
373
|
+
const cookieless = this.isCookieless();
|
|
374
|
+
const userId = getAnonymousId(this.options.siteKey, cookieless);
|
|
375
|
+
const session = getSessionContext(this.options.siteKey, cookieless);
|
|
376
|
+
let data;
|
|
377
|
+
try {
|
|
378
|
+
data = JSON.stringify({
|
|
379
|
+
token: this.options.siteKey,
|
|
380
|
+
...userId ? { userId } : {},
|
|
381
|
+
sessionId: session.sessionId,
|
|
382
|
+
windowId: session.windowId,
|
|
383
|
+
event,
|
|
384
|
+
...getPageContext(this.options.trackHash ?? false),
|
|
385
|
+
...dimensions,
|
|
386
|
+
properties
|
|
387
|
+
});
|
|
388
|
+
} catch (error) {
|
|
389
|
+
if (this.debug) console.log("serialization failed", event, error);
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
sendData({
|
|
393
|
+
url: `${this.baseUrl}${URLS.events}`,
|
|
394
|
+
data,
|
|
395
|
+
contentType: "text/plain",
|
|
396
|
+
debug: this.debug,
|
|
397
|
+
debugPrefix: "[FastStats]"
|
|
398
|
+
});
|
|
441
399
|
}
|
|
442
400
|
};
|
|
443
401
|
//#endregion
|
|
444
|
-
|
|
402
|
+
//#region src/client.ts
|
|
403
|
+
let client;
|
|
404
|
+
/** Creates an independent client. Call `start()` when it should begin tracking. */
|
|
405
|
+
function createClient(options) {
|
|
406
|
+
return new WebAnalytics(options);
|
|
407
|
+
}
|
|
408
|
+
/** Initializes and starts the shared client used by the convenience functions. */
|
|
409
|
+
function init(options) {
|
|
410
|
+
if (client) return client;
|
|
411
|
+
client = createClient(options);
|
|
412
|
+
client.start();
|
|
413
|
+
return client;
|
|
414
|
+
}
|
|
415
|
+
/** Stops and forgets the shared client. Independent clients are unaffected. */
|
|
416
|
+
function shutdown() {
|
|
417
|
+
client?.destroy();
|
|
418
|
+
client = void 0;
|
|
419
|
+
}
|
|
420
|
+
function pageview(properties) {
|
|
421
|
+
client?.pageview(properties);
|
|
422
|
+
}
|
|
423
|
+
function track(name, properties) {
|
|
424
|
+
client?.track(name, properties);
|
|
425
|
+
}
|
|
426
|
+
function identify(userOrExternalId, email, options) {
|
|
427
|
+
if (!client) return Promise.resolve(false);
|
|
428
|
+
return typeof userOrExternalId === "string" ? client.identify(userOrExternalId, email, options) : client.identify(userOrExternalId);
|
|
429
|
+
}
|
|
430
|
+
function logout(resetAnonymousIdentity) {
|
|
431
|
+
client?.logout(resetAnonymousIdentity);
|
|
432
|
+
}
|
|
433
|
+
function setConsentMode(mode) {
|
|
434
|
+
client?.setConsentMode(mode);
|
|
435
|
+
}
|
|
436
|
+
function reportError(error) {
|
|
437
|
+
client?.reportError(error);
|
|
438
|
+
}
|
|
439
|
+
//#endregion
|
|
440
|
+
export { WebAnalytics, createClient, identify, init, logout, pageview, reportError, setConsentMode, shutdown, track };
|