@mate-academy/analytics-client 1.0.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.
@@ -0,0 +1,44 @@
1
+ import { Types } from '@amplitude/analytics-browser';
2
+
3
+ interface AnalyticsPlugin {
4
+ name: string;
5
+ init?(): void;
6
+ track(payload: {
7
+ event: string;
8
+ properties: Record<string, unknown>;
9
+ }): void;
10
+ identify?(userId: string, traits?: Record<string, unknown>): void;
11
+ reset?(): void;
12
+ }
13
+
14
+ declare function registerPlugins(newPlugins: AnalyticsPlugin[]): void;
15
+ declare function track(event: string, properties?: Record<string, unknown>): void;
16
+ declare function identify(userId: string, traits?: Record<string, unknown>): void;
17
+ declare function reset(): void;
18
+
19
+ interface DwhErrorContext {
20
+ eventType: string;
21
+ eventProperties: Record<string, unknown>;
22
+ attempts: number;
23
+ }
24
+ interface DwhPluginOptions {
25
+ subDomain?: string;
26
+ dwhEndpoint?: string;
27
+ cookieDomain?: string;
28
+ onError?: (error: unknown, context: DwhErrorContext) => void;
29
+ }
30
+ declare function createDwhPlugin(options?: DwhPluginOptions): AnalyticsPlugin;
31
+
32
+ interface AmplitudePluginOptions {
33
+ apiKey: string;
34
+ config?: Partial<Types.BrowserOptions>;
35
+ }
36
+ declare function createAmplitudePlugin(options: AmplitudePluginOptions): AnalyticsPlugin;
37
+
38
+ declare const PLUGIN_NAMES: {
39
+ readonly DWH: "dwh";
40
+ readonly AMPLITUDE: "amplitude";
41
+ };
42
+ type PluginName = typeof PLUGIN_NAMES[keyof typeof PLUGIN_NAMES];
43
+
44
+ export { type AnalyticsPlugin, type DwhErrorContext, PLUGIN_NAMES, type PluginName, createAmplitudePlugin, createDwhPlugin, identify, registerPlugins, reset, track };
package/dist/index.js ADDED
@@ -0,0 +1,409 @@
1
+ // src/tracker.ts
2
+ var plugins = [];
3
+ var isInitialized = false;
4
+ var queue = [];
5
+ function safeCall(plugin, fn) {
6
+ try {
7
+ fn();
8
+ } catch (error) {
9
+ console.error(`[analytics] plugin "${plugin.name}" failed:`, error);
10
+ }
11
+ }
12
+ function dispatch(call) {
13
+ plugins.forEach((plugin) => {
14
+ safeCall(plugin, () => {
15
+ if (call.type === "track") {
16
+ plugin.track({ event: call.event, properties: call.properties });
17
+ } else if (call.type === "identify") {
18
+ plugin.identify?.(call.userId, call.traits);
19
+ } else if (call.type === "reset") {
20
+ plugin.reset?.();
21
+ }
22
+ });
23
+ });
24
+ }
25
+ function registerPlugins(newPlugins) {
26
+ plugins = newPlugins;
27
+ plugins.forEach((plugin) => {
28
+ safeCall(plugin, () => plugin.init?.());
29
+ });
30
+ isInitialized = true;
31
+ while (queue.length > 0) {
32
+ const call = queue.shift();
33
+ if (call) {
34
+ dispatch(call);
35
+ }
36
+ }
37
+ }
38
+ function track(event, properties = {}) {
39
+ if (!isInitialized) {
40
+ queue.push({ type: "track", event, properties });
41
+ return;
42
+ }
43
+ dispatch({ type: "track", event, properties });
44
+ }
45
+ function identify(userId, traits) {
46
+ if (!isInitialized) {
47
+ queue.push({ type: "identify", userId, traits });
48
+ return;
49
+ }
50
+ dispatch({ type: "identify", userId, traits });
51
+ }
52
+ function reset() {
53
+ if (!isInitialized) {
54
+ queue.push({ type: "reset" });
55
+ return;
56
+ }
57
+ dispatch({ type: "reset" });
58
+ }
59
+
60
+ // src/plugins/dwh.ts
61
+ import sbjs from "@mate-academy/sourcebuster";
62
+
63
+ // src/cookies.ts
64
+ import { parse } from "cookie";
65
+ function getCookieValue(name) {
66
+ try {
67
+ return parse(document.cookie)[name] ?? null;
68
+ } catch {
69
+ return null;
70
+ }
71
+ }
72
+ function setCookie(name, value, maxAge) {
73
+ document.cookie = `${name}=${value}; path=/; max-age=${maxAge}`;
74
+ }
75
+
76
+ // src/device-id.ts
77
+ var USER_DEVICE_ID_KEY = "_mate-user-device-id";
78
+ function base64Id() {
79
+ const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
80
+ let result = "";
81
+ for (let index = 0; index < 22; index += 1) {
82
+ result += chars.charAt(Math.floor(Math.random() * 64));
83
+ }
84
+ return result;
85
+ }
86
+ function getOrCreateDeviceId() {
87
+ const fromCookie = getCookieValue(USER_DEVICE_ID_KEY);
88
+ if (fromCookie) {
89
+ try {
90
+ localStorage.setItem(USER_DEVICE_ID_KEY, fromCookie);
91
+ } catch {
92
+ }
93
+ return fromCookie;
94
+ }
95
+ let fromStorage = null;
96
+ try {
97
+ fromStorage = localStorage.getItem(USER_DEVICE_ID_KEY);
98
+ } catch {
99
+ }
100
+ if (fromStorage) {
101
+ setCookie(USER_DEVICE_ID_KEY, fromStorage, 31536e3);
102
+ return fromStorage;
103
+ }
104
+ const deviceId = base64Id();
105
+ try {
106
+ localStorage.setItem(USER_DEVICE_ID_KEY, deviceId);
107
+ } catch {
108
+ }
109
+ setCookie(USER_DEVICE_ID_KEY, deviceId, 31536e3);
110
+ return deviceId;
111
+ }
112
+
113
+ // src/constants.ts
114
+ var PLUGIN_NAMES = {
115
+ DWH: "dwh",
116
+ AMPLITUDE: "amplitude"
117
+ };
118
+
119
+ // src/plugins/dwh.ts
120
+ var USER_ANALYTICS_ID_KEY = "_mate-user-analytics-id";
121
+ var USER_SESSION_ID_KEY = "_mate-user-session-id";
122
+ var SESSION_MAX_AGE_SECONDS = 1800;
123
+ var MAX_SEND_ATTEMPTS = 3;
124
+ function createDwhPlugin(options = {}) {
125
+ const subDomain = options.subDomain ?? "ua";
126
+ const dwhEndpoint = options.dwhEndpoint ?? "/dwh";
127
+ const cookieDomain = options.cookieDomain ?? "mate.academy";
128
+ const onError = options.onError;
129
+ let sourceTracker;
130
+ function getUserIp() {
131
+ return getCookieValue("_mate-user-ip") || "8.8.8.8";
132
+ }
133
+ function getUserId() {
134
+ let fromStorage = null;
135
+ try {
136
+ fromStorage = localStorage.getItem(USER_ANALYTICS_ID_KEY);
137
+ } catch {
138
+ }
139
+ if (fromStorage) {
140
+ setCookie(USER_ANALYTICS_ID_KEY, fromStorage, 31536e3);
141
+ return fromStorage;
142
+ }
143
+ const fromCookie = getCookieValue(USER_ANALYTICS_ID_KEY);
144
+ if (fromCookie) {
145
+ try {
146
+ localStorage.setItem(USER_ANALYTICS_ID_KEY, fromCookie);
147
+ } catch {
148
+ }
149
+ return fromCookie;
150
+ }
151
+ return null;
152
+ }
153
+ function ensureSessionId() {
154
+ const sourceTrackerSessionId = sourceTracker?.get?.session?.sid;
155
+ const currentSessionId = getCookieValue(USER_SESSION_ID_KEY);
156
+ const isSessionChanged = sourceTrackerSessionId && currentSessionId && Number(sourceTrackerSessionId) > Number(currentSessionId);
157
+ const shouldSetNewSessionId = !currentSessionId || isSessionChanged;
158
+ if (shouldSetNewSessionId) {
159
+ const sessionId = Date.now().toString();
160
+ setCookie(USER_SESSION_ID_KEY, sessionId, SESSION_MAX_AGE_SECONDS);
161
+ return { sessionId, isNewSession: true };
162
+ }
163
+ setCookie(USER_SESSION_ID_KEY, currentSessionId, SESSION_MAX_AGE_SECONDS);
164
+ return { sessionId: currentSessionId, isNewSession: false };
165
+ }
166
+ function getFirstVisitPage() {
167
+ if (!sourceTracker) {
168
+ return null;
169
+ }
170
+ const entrancePoint = sourceTracker.get.first_add.ep;
171
+ if (!entrancePoint) {
172
+ return null;
173
+ }
174
+ try {
175
+ const pathname = new URL(entrancePoint).pathname.slice(1);
176
+ return pathname || "home";
177
+ } catch {
178
+ return entrancePoint;
179
+ }
180
+ }
181
+ function getSourceData() {
182
+ if (!sourceTracker) {
183
+ return {
184
+ fvPage: null,
185
+ lvType: null,
186
+ lvSource: null,
187
+ lvMedium: null,
188
+ lvCampaign: null,
189
+ lvContent: null,
190
+ lvTerm: null,
191
+ lvAccountId: null,
192
+ lvCampaignId: null,
193
+ lvAdGroupId: null,
194
+ lvAdId: null,
195
+ lvTermId: null
196
+ };
197
+ }
198
+ const current = sourceTracker.get.current;
199
+ return {
200
+ fvPage: getFirstVisitPage(),
201
+ lvType: current.typ,
202
+ lvSource: current.src,
203
+ lvMedium: current.mdm,
204
+ lvCampaign: current.cmp,
205
+ lvContent: current.cnt,
206
+ lvTerm: current.trm,
207
+ lvAccountId: current.ac_id,
208
+ lvCampaignId: current.cmp_id,
209
+ lvAdGroupId: current.adg_id,
210
+ lvAdId: current.ad_id,
211
+ lvTermId: current.trm_id
212
+ };
213
+ }
214
+ function getFacebookParams() {
215
+ const fbp = getCookieValue("_fbp");
216
+ let fbc = getCookieValue("_fbc");
217
+ let fbclid = getCookieValue("_mate-fbclid");
218
+ if (fbclid?.includes("https://")) {
219
+ const queryParamsString = fbclid.split("?")[1];
220
+ const queryParams = new URLSearchParams(queryParamsString);
221
+ fbclid = queryParams.get("fbclid") ?? null;
222
+ }
223
+ if (!fbc && fbclid) {
224
+ fbc = `fb.1.${Date.now()}.${fbclid}`;
225
+ }
226
+ if (fbc?.includes("https://")) {
227
+ const queryParamsString = fbc.split("?")[1];
228
+ const fbcTimestamp = fbc.split(".")[2] ?? Date.now().toString();
229
+ const queryParams = new URLSearchParams(queryParamsString);
230
+ fbclid = queryParams.get("fbclid") ?? null;
231
+ fbc = `fb.1.${fbcTimestamp}.${fbclid}`;
232
+ }
233
+ return { fbp, fbc, fbclid };
234
+ }
235
+ function getClickIds() {
236
+ const facebookParams = getFacebookParams();
237
+ return {
238
+ gclid: getCookieValue("_mate-gclid"),
239
+ gbraid: getCookieValue("_mate-gbraid"),
240
+ wbraid: getCookieValue("_mate-wbraid"),
241
+ gClientid: getCookieValue("_ga") ?? "0.0",
242
+ fbc: facebookParams.fbc,
243
+ fbp: facebookParams.fbp,
244
+ fbclid: facebookParams.fbclid,
245
+ ttclid: getCookieValue("_mate-ttclid"),
246
+ sdclid: getCookieValue("_mate-sdclid"),
247
+ ksclid: getCookieValue("_mate-ksclid")
248
+ };
249
+ }
250
+ function getTrackingData(sessionId) {
251
+ return {
252
+ ...getSourceData(),
253
+ ...getClickIds(),
254
+ locationIpAddress: getUserIp(),
255
+ sessionId,
256
+ userAgent: sourceTracker ? sourceTracker.get.udata.uag : navigator.userAgent
257
+ };
258
+ }
259
+ function sleep(ms) {
260
+ return new Promise((resolve) => {
261
+ setTimeout(resolve, ms);
262
+ });
263
+ }
264
+ async function sendPayload(payload) {
265
+ const body = JSON.stringify(payload);
266
+ if (typeof navigator !== "undefined" && navigator.sendBeacon) {
267
+ const blob = new Blob([body], { type: "application/json" });
268
+ const scheduled = navigator.sendBeacon(dwhEndpoint, blob);
269
+ if (scheduled) {
270
+ return;
271
+ }
272
+ }
273
+ const response = await fetch(dwhEndpoint, {
274
+ method: "POST",
275
+ body,
276
+ credentials: "omit",
277
+ keepalive: true,
278
+ headers: { "Content-Type": "application/json" }
279
+ });
280
+ if (!response.ok) {
281
+ throw new Error(`DWH response not ok: ${response.statusText}`);
282
+ }
283
+ }
284
+ async function sendDwhEvent(eventType, eventProperties, attempt = 1) {
285
+ try {
286
+ const { sessionId, isNewSession } = ensureSessionId();
287
+ const userId = getUserId();
288
+ const deviceId = getOrCreateDeviceId();
289
+ const tracking = getTrackingData(sessionId);
290
+ const basePayload = {
291
+ session_id: sessionId,
292
+ platform_user_id: userId,
293
+ platform_device_id: deviceId,
294
+ ...tracking
295
+ };
296
+ if (isNewSession) {
297
+ await sendPayload({
298
+ event_type: "session_start",
299
+ location: window.location.pathname,
300
+ event_properties: { subDomain },
301
+ ...basePayload
302
+ });
303
+ }
304
+ await sendPayload({
305
+ event_type: eventType,
306
+ location: window.location.pathname,
307
+ event_properties: {
308
+ subDomain,
309
+ ...eventProperties
310
+ },
311
+ ...basePayload
312
+ });
313
+ } catch (error) {
314
+ if (attempt < MAX_SEND_ATTEMPTS) {
315
+ const delayTime = 100 * 2 ** (attempt - 1);
316
+ await sleep(delayTime);
317
+ return sendDwhEvent(eventType, eventProperties, attempt + 1);
318
+ }
319
+ onError?.(error, {
320
+ eventType,
321
+ eventProperties,
322
+ attempts: attempt
323
+ });
324
+ }
325
+ }
326
+ return {
327
+ name: PLUGIN_NAMES.DWH,
328
+ init() {
329
+ sourceTracker = window.sbjs || sbjs;
330
+ if (sourceTracker !== window.sbjs) {
331
+ sourceTracker.init({
332
+ domain: cookieDomain,
333
+ user_ip: getUserIp()
334
+ });
335
+ window.sbjs = sourceTracker;
336
+ }
337
+ },
338
+ track({ event, properties }) {
339
+ sendDwhEvent(event, properties);
340
+ },
341
+ reset() {
342
+ try {
343
+ localStorage.removeItem(USER_ANALYTICS_ID_KEY);
344
+ } catch {
345
+ }
346
+ setCookie(USER_ANALYTICS_ID_KEY, "", 0);
347
+ setCookie(USER_SESSION_ID_KEY, "", 0);
348
+ }
349
+ };
350
+ }
351
+
352
+ // src/plugins/amplitude.ts
353
+ import { createInstance, Identify } from "@amplitude/analytics-browser";
354
+ function createAmplitudePlugin(options) {
355
+ let client = null;
356
+ return {
357
+ name: PLUGIN_NAMES.AMPLITUDE,
358
+ init() {
359
+ client = createInstance();
360
+ const deviceId = getOrCreateDeviceId();
361
+ client.init(options.apiKey, {
362
+ deviceId,
363
+ autocapture: {
364
+ attribution: false,
365
+ pageViews: false,
366
+ sessions: false,
367
+ fileDownloads: false,
368
+ formInteractions: false
369
+ },
370
+ identityStorage: "localStorage",
371
+ ...options.config
372
+ });
373
+ },
374
+ track({ event, properties }) {
375
+ if (!client) {
376
+ return;
377
+ }
378
+ client.track(event, properties);
379
+ },
380
+ identify(userId, traits) {
381
+ if (!client) {
382
+ return;
383
+ }
384
+ client.setUserId(userId);
385
+ if (traits) {
386
+ const userProperties = new Identify();
387
+ Object.entries(traits).forEach(([key, value]) => {
388
+ userProperties.set(key, value);
389
+ });
390
+ client.identify(userProperties);
391
+ }
392
+ },
393
+ reset() {
394
+ if (!client) {
395
+ return;
396
+ }
397
+ client.reset();
398
+ }
399
+ };
400
+ }
401
+ export {
402
+ PLUGIN_NAMES,
403
+ createAmplitudePlugin,
404
+ createDwhPlugin,
405
+ identify,
406
+ registerPlugins,
407
+ reset,
408
+ track
409
+ };
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "@mate-academy/analytics-client",
3
+ "version": "1.0.0",
4
+ "description": "Pluggable analytics client for Mate academy external pages",
5
+ "type": "module",
6
+ "exports": {
7
+ ".": {
8
+ "types": "./dist/index.d.ts",
9
+ "import": "./dist/index.js"
10
+ }
11
+ },
12
+ "files": [
13
+ "dist/"
14
+ ],
15
+ "scripts": {
16
+ "build": "tsup"
17
+ },
18
+ "dependencies": {
19
+ "@amplitude/analytics-browser": "^2.40.0",
20
+ "@mate-academy/sourcebuster": "^1.5.0",
21
+ "cookie": "^1.0.1"
22
+ },
23
+ "devDependencies": {
24
+ "@types/cookie": "^0.6.0",
25
+ "@typescript-eslint/parser": "^8.58.1",
26
+ "tsup": "^8.0.0",
27
+ "typescript": "^5.8.0"
28
+ },
29
+ "publishConfig": {
30
+ "access": "public"
31
+ },
32
+ "repository": {
33
+ "type": "git",
34
+ "url": "https://github.com/mate-academy/website.git",
35
+ "directory": "packages/mate-analytics"
36
+ },
37
+ "license": "MIT"
38
+ }