@armco/analytics 0.2.11 → 0.3.1

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.
Files changed (44) hide show
  1. package/core/analytics.d.ts +49 -0
  2. package/core/analytics.js +354 -0
  3. package/core/errors.d.ts +26 -0
  4. package/core/errors.js +54 -0
  5. package/core/types.d.ts +137 -0
  6. package/core/types.js +1 -0
  7. package/global-modules.d.ts +14 -0
  8. package/index.d.ts +24 -0
  9. package/index.js +24 -0
  10. package/package.json +6 -36
  11. package/plugins/auto-track/click.d.ts +15 -0
  12. package/plugins/auto-track/click.js +99 -0
  13. package/plugins/auto-track/error.d.ts +15 -0
  14. package/plugins/auto-track/error.js +65 -0
  15. package/plugins/auto-track/form.d.ts +13 -0
  16. package/plugins/auto-track/form.js +54 -0
  17. package/plugins/auto-track/page.d.ts +14 -0
  18. package/plugins/auto-track/page.js +72 -0
  19. package/plugins/enrichment/session.d.ts +18 -0
  20. package/plugins/enrichment/session.js +81 -0
  21. package/plugins/enrichment/user.d.ts +20 -0
  22. package/plugins/enrichment/user.js +159 -0
  23. package/plugins/node/http-request-tracking.d.ts +54 -0
  24. package/plugins/node/http-request-tracking.js +158 -0
  25. package/storage/cookie-storage.d.ts +8 -0
  26. package/storage/cookie-storage.js +60 -0
  27. package/storage/hybrid-storage.d.ts +12 -0
  28. package/storage/hybrid-storage.js +108 -0
  29. package/storage/local-storage.d.ts +8 -0
  30. package/storage/local-storage.js +65 -0
  31. package/storage/memory-storage.d.ts +9 -0
  32. package/storage/memory-storage.js +22 -0
  33. package/transport/beacon-transport.d.ts +16 -0
  34. package/transport/beacon-transport.js +50 -0
  35. package/transport/fetch-transport.d.ts +20 -0
  36. package/transport/fetch-transport.js +112 -0
  37. package/utils/config-loader.d.ts +6 -0
  38. package/utils/config-loader.js +168 -0
  39. package/utils/helpers.d.ts +15 -0
  40. package/utils/helpers.js +148 -0
  41. package/utils/logging.d.ts +17 -0
  42. package/utils/logging.js +54 -0
  43. package/utils/validation.d.ts +9 -0
  44. package/utils/validation.js +146 -0
package/package.json CHANGED
@@ -1,25 +1,11 @@
1
1
  {
2
2
  "name": "@armco/analytics",
3
- "version": "0.2.11",
3
+ "version": "0.3.1",
4
4
  "description": "Universal Analytics Library for Browser and Node.js",
5
- "main": "dist/index.js",
6
- "types": "dist/index.d.ts",
5
+ "main": "index.js",
6
+ "types": "index.d.ts",
7
7
  "type": "module",
8
- "scripts": {
9
- "build": "npx ts-node build.js",
10
- "lint": "npx eslint --ext .ts src/",
11
- "lint:tests": "npx eslint --ext .ts tests/",
12
- "start": "node ./dist --env=production",
13
- "dev": "nodemon",
14
- "test": "jest",
15
- "test:watch": "jest --watch",
16
- "test:coverage": "jest --coverage",
17
- "test:unit": "jest --testPathPattern=tests/unit",
18
- "test:integration": "jest --testPathPattern=tests/integration",
19
- "publish:local": "./publish-local.sh",
20
- "publish:sh": "./publish.sh",
21
- "publish:sh:minor": "./publish.sh minor"
22
- },
8
+ "scripts": {},
23
9
  "repository": {
24
10
  "type": "git",
25
11
  "url": "git+https://github.com/ReStruct-Corporate-Advantage/analytics.git"
@@ -37,27 +23,11 @@
37
23
  "url": "https://github.com/ReStruct-Corporate-Advantage/analytics/issues"
38
24
  },
39
25
  "homepage": "https://github.com/ReStruct-Corporate-Advantage/analytics#readme",
40
- "files": [
41
- "dist",
42
- "README.md",
43
- "LICENSE"
44
- ],
45
- "devDependencies": {
46
- "@jest/globals": "^29.7.0",
47
- "@types/fs-extra": "^11.0.1",
48
- "@types/jest": "^29.5.11",
49
- "@types/js-cookie": "^3.0.3",
50
- "@types/node": "^20.4.2",
51
- "@types/uuid": "^9.0.2",
52
- "fs-extra": "^11.1.1",
53
- "jest": "^29.7.0",
54
- "ts-jest": "^29.1.1",
55
- "typescript": "^5.1.6"
56
- },
26
+ "devDependencies": {},
57
27
  "dependencies": {
58
28
  "js-cookie": "^3.0.5",
59
29
  "jstz": "^2.1.1",
60
30
  "uuid": "^9.0.0",
61
31
  "zod": "^4.1.13"
62
32
  }
63
- }
33
+ }
@@ -0,0 +1,15 @@
1
+ import type { Plugin, PluginContext } from "../../core/types";
2
+ export declare class ClickTrackingPlugin implements Plugin {
3
+ name: string;
4
+ version: string;
5
+ private context?;
6
+ private logger;
7
+ private boundHandler?;
8
+ init(context: PluginContext): void;
9
+ private attachHandlers;
10
+ private handleClick;
11
+ private isTrackable;
12
+ private extractClickData;
13
+ private getElementPath;
14
+ destroy(): void;
15
+ }
@@ -0,0 +1,99 @@
1
+ import { isBrowser } from "../../utils/helpers";
2
+ import { getLogger } from "../../utils/logging";
3
+ const TRACKED_ELEMENTS = [
4
+ "a[href]",
5
+ "button",
6
+ "input[type='button']",
7
+ "input[type='submit']",
8
+ "input[type='reset']",
9
+ "[role='button']",
10
+ "[role='link']",
11
+ "[data-track='true']",
12
+ ];
13
+ export class ClickTrackingPlugin {
14
+ constructor() {
15
+ this.name = "ClickTrackingPlugin";
16
+ this.version = "1.0.0";
17
+ this.logger = getLogger();
18
+ }
19
+ init(context) {
20
+ if (!isBrowser()) {
21
+ this.logger.warn("Click tracking only available in browser");
22
+ return;
23
+ }
24
+ this.context = context;
25
+ this.logger.info("Attaching Click handlers");
26
+ this.attachHandlers();
27
+ const trackableElements = document.querySelectorAll(TRACKED_ELEMENTS.join(", "));
28
+ this.logger.info(`Found ${trackableElements.length} items that can be clicked!`);
29
+ this.logger.info("Dynamically added elements will be added to this list.");
30
+ this.logger.info("Click handlers Attached");
31
+ }
32
+ attachHandlers() {
33
+ this.boundHandler = this.handleClick.bind(this);
34
+ document.addEventListener("click", this.boundHandler, true);
35
+ }
36
+ handleClick(e) {
37
+ const element = e.target;
38
+ if (!this.isTrackable(element)) {
39
+ return;
40
+ }
41
+ const clickData = this.extractClickData(element);
42
+ if (this.context) {
43
+ this.context.track("CLICK", clickData);
44
+ }
45
+ }
46
+ isTrackable(element) {
47
+ return (element.matches(TRACKED_ELEMENTS.join(", ")) ||
48
+ element.onclick != null ||
49
+ window.getComputedStyle(element).cursor === "pointer");
50
+ }
51
+ extractClickData(element) {
52
+ const data = {
53
+ elementType: element.tagName.toLowerCase(),
54
+ elementId: element.id || undefined,
55
+ elementText: element.textContent?.trim() || undefined,
56
+ elementClasses: Array.from(element.classList),
57
+ elementPath: this.getElementPath(element),
58
+ };
59
+ if ("href" in element) {
60
+ data.href = element.href;
61
+ }
62
+ if ("value" in element && element.value) {
63
+ data.value = element.value;
64
+ }
65
+ const dataAttributes = { ...element.dataset };
66
+ if (Object.keys(dataAttributes).length > 0) {
67
+ data.dataAttributes = dataAttributes;
68
+ }
69
+ return data;
70
+ }
71
+ getElementPath(element) {
72
+ const path = [];
73
+ let current = element;
74
+ while (current && current !== document.body) {
75
+ let selector = current.tagName.toLowerCase();
76
+ if (current.id) {
77
+ selector += `#${current.id}`;
78
+ path.unshift(selector);
79
+ break;
80
+ }
81
+ else if (current.className) {
82
+ const classes = Array.from(current.classList)
83
+ .filter((c) => c.trim())
84
+ .join(".");
85
+ if (classes) {
86
+ selector += `.${classes}`;
87
+ }
88
+ }
89
+ path.unshift(selector);
90
+ current = current.parentElement;
91
+ }
92
+ return path.join(" > ");
93
+ }
94
+ destroy() {
95
+ if (this.boundHandler) {
96
+ document.removeEventListener("click", this.boundHandler, true);
97
+ }
98
+ }
99
+ }
@@ -0,0 +1,15 @@
1
+ import type { Plugin, PluginContext } from "../../core/types";
2
+ export declare class ErrorTrackingPlugin implements Plugin {
3
+ name: string;
4
+ version: string;
5
+ private context?;
6
+ private logger;
7
+ private boundErrorHandler?;
8
+ private boundRejectionHandler?;
9
+ init(context: PluginContext): void;
10
+ private attachHandlers;
11
+ private handleError;
12
+ private handleRejection;
13
+ trackError(error: Error | string): void;
14
+ destroy(): void;
15
+ }
@@ -0,0 +1,65 @@
1
+ import { isBrowser } from "../../utils/helpers";
2
+ import { getLogger } from "../../utils/logging";
3
+ export class ErrorTrackingPlugin {
4
+ constructor() {
5
+ this.name = "ErrorTrackingPlugin";
6
+ this.version = "1.0.0";
7
+ this.logger = getLogger();
8
+ }
9
+ init(context) {
10
+ if (!isBrowser()) {
11
+ this.logger.warn("Error tracking only available in browser");
12
+ return;
13
+ }
14
+ this.context = context;
15
+ this.attachHandlers();
16
+ this.logger.info("Error tracking initialized");
17
+ }
18
+ attachHandlers() {
19
+ this.boundErrorHandler = this.handleError.bind(this);
20
+ window.addEventListener("error", this.boundErrorHandler);
21
+ this.boundRejectionHandler = this.handleRejection.bind(this);
22
+ window.addEventListener("unhandledrejection", this.boundRejectionHandler);
23
+ }
24
+ handleError(e) {
25
+ const errorData = {
26
+ errorMessage: e.message,
27
+ errorStack: e.error?.stack,
28
+ errorType: e.error?.name || "Error",
29
+ filename: e.filename,
30
+ lineNumber: e.lineno,
31
+ columnNumber: e.colno,
32
+ };
33
+ if (this.context) {
34
+ this.context.track("ERROR", errorData);
35
+ }
36
+ }
37
+ handleRejection(e) {
38
+ const errorData = {
39
+ errorMessage: e.reason?.message || String(e.reason),
40
+ errorStack: e.reason?.stack,
41
+ errorType: "UnhandledPromiseRejection",
42
+ };
43
+ if (this.context) {
44
+ this.context.track("ERROR", errorData);
45
+ }
46
+ }
47
+ trackError(error) {
48
+ const errorData = {
49
+ errorMessage: typeof error === "string" ? error : error.message,
50
+ errorStack: typeof error === "string" ? undefined : error.stack,
51
+ errorType: typeof error === "string" ? "Error" : error.name,
52
+ };
53
+ if (this.context) {
54
+ this.context.track("ERROR", errorData);
55
+ }
56
+ }
57
+ destroy() {
58
+ if (this.boundErrorHandler) {
59
+ window.removeEventListener("error", this.boundErrorHandler);
60
+ }
61
+ if (this.boundRejectionHandler) {
62
+ window.removeEventListener("unhandledrejection", this.boundRejectionHandler);
63
+ }
64
+ }
65
+ }
@@ -0,0 +1,13 @@
1
+ import type { Plugin, PluginContext } from "../../core/types";
2
+ export declare class FormTrackingPlugin implements Plugin {
3
+ name: string;
4
+ version: string;
5
+ private context?;
6
+ private logger;
7
+ private boundHandler?;
8
+ init(context: PluginContext): void;
9
+ private attachHandlers;
10
+ private handleSubmit;
11
+ private extractFormData;
12
+ destroy(): void;
13
+ }
@@ -0,0 +1,54 @@
1
+ import { isBrowser } from "../../utils/helpers";
2
+ import { getLogger } from "../../utils/logging";
3
+ export class FormTrackingPlugin {
4
+ constructor() {
5
+ this.name = "FormTrackingPlugin";
6
+ this.version = "1.0.0";
7
+ this.logger = getLogger();
8
+ }
9
+ init(context) {
10
+ if (!isBrowser()) {
11
+ this.logger.warn("Form tracking only available in browser");
12
+ return;
13
+ }
14
+ this.context = context;
15
+ this.attachHandlers();
16
+ this.logger.info("Form tracking initialized");
17
+ }
18
+ attachHandlers() {
19
+ this.boundHandler = this.handleSubmit.bind(this);
20
+ document.addEventListener("submit", this.boundHandler, true);
21
+ }
22
+ handleSubmit(e) {
23
+ const form = e.target;
24
+ const formData = this.extractFormData(form);
25
+ if (this.context) {
26
+ this.context.track("FORM_SUBMIT", formData);
27
+ }
28
+ }
29
+ extractFormData(form) {
30
+ const data = {
31
+ formId: form.id || undefined,
32
+ formName: form.name || undefined,
33
+ formAction: form.action || undefined,
34
+ formMethod: form.method || undefined,
35
+ };
36
+ const fields = [];
37
+ const formElements = form.elements;
38
+ for (let i = 0; i < formElements.length; i++) {
39
+ const element = formElements[i];
40
+ if (element.name) {
41
+ fields.push(element.name);
42
+ }
43
+ }
44
+ if (fields.length > 0) {
45
+ data.fields = fields;
46
+ }
47
+ return data;
48
+ }
49
+ destroy() {
50
+ if (this.boundHandler) {
51
+ document.removeEventListener("submit", this.boundHandler, true);
52
+ }
53
+ }
54
+ }
@@ -0,0 +1,14 @@
1
+ import type { Plugin, PluginContext, PageViewEvent } from "../../core/types";
2
+ export declare class PageTrackingPlugin implements Plugin {
3
+ name: string;
4
+ version: string;
5
+ private context?;
6
+ private logger;
7
+ private lastUrl?;
8
+ init(context: PluginContext): void;
9
+ private attachHandlers;
10
+ private trackCurrentPage;
11
+ private getPageName;
12
+ trackPage(data: Partial<PageViewEvent>): void;
13
+ destroy(): void;
14
+ }
@@ -0,0 +1,72 @@
1
+ import { isBrowser } from "../../utils/helpers";
2
+ import { getLogger } from "../../utils/logging";
3
+ export class PageTrackingPlugin {
4
+ constructor() {
5
+ this.name = "PageTrackingPlugin";
6
+ this.version = "1.0.0";
7
+ this.logger = getLogger();
8
+ }
9
+ init(context) {
10
+ if (!isBrowser()) {
11
+ this.logger.warn("Page tracking only available in browser");
12
+ return;
13
+ }
14
+ this.context = context;
15
+ this.attachHandlers();
16
+ this.logger.info("Logging page load");
17
+ this.trackCurrentPage();
18
+ this.logger.info("Page tracking initialized");
19
+ }
20
+ attachHandlers() {
21
+ window.addEventListener("load", () => this.trackCurrentPage());
22
+ window.addEventListener("popstate", () => this.trackCurrentPage());
23
+ window.addEventListener("hashchange", () => this.trackCurrentPage());
24
+ }
25
+ trackCurrentPage() {
26
+ const url = window.location.href;
27
+ if (url === this.lastUrl) {
28
+ return;
29
+ }
30
+ this.lastUrl = url;
31
+ const pageData = {
32
+ pageName: this.getPageName(),
33
+ url: url,
34
+ referrer: document.referrer || undefined,
35
+ title: document.title || undefined,
36
+ };
37
+ if (this.context) {
38
+ this.context.track("PAGE_VIEW", pageData);
39
+ }
40
+ }
41
+ getPageName() {
42
+ if (document.title) {
43
+ return document.title;
44
+ }
45
+ const pathname = window.location.pathname;
46
+ if (pathname === "/" || pathname === "") {
47
+ return "Home";
48
+ }
49
+ return pathname
50
+ .split("/")
51
+ .filter((part) => part)
52
+ .map((part) => part
53
+ .split("-")
54
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
55
+ .join(" "))
56
+ .join(" - ");
57
+ }
58
+ trackPage(data) {
59
+ const pageData = {
60
+ pageName: data.pageName || this.getPageName(),
61
+ url: data.url || window.location.href,
62
+ referrer: data.referrer || document.referrer || undefined,
63
+ title: data.title || document.title || undefined,
64
+ };
65
+ if (this.context) {
66
+ this.context.track("PAGE_VIEW", pageData);
67
+ }
68
+ this.lastUrl = pageData.url;
69
+ }
70
+ destroy() {
71
+ }
72
+ }
@@ -0,0 +1,18 @@
1
+ import type { Plugin, PluginContext, TrackingEvent } from "../../core/types";
2
+ export declare class SessionPlugin implements Plugin {
3
+ name: string;
4
+ version: string;
5
+ private context?;
6
+ private sessionId;
7
+ private tabId;
8
+ private logger;
9
+ init(context: PluginContext): void;
10
+ processEvent(event: TrackingEvent): void;
11
+ private startSession;
12
+ private getTabId;
13
+ private storeSession;
14
+ private extendSession;
15
+ getSessionId(): string | null;
16
+ terminateSession(): void;
17
+ destroy(): void;
18
+ }
@@ -0,0 +1,81 @@
1
+ import { generateId } from "../../utils/helpers";
2
+ import { getLogger } from "../../utils/logging";
3
+ const SESSION_KEY = "ar_session_id";
4
+ const TAB_KEY = "ar_tab_id";
5
+ const SESSION_EXPIRATION_MINUTES = 30;
6
+ export class SessionPlugin {
7
+ constructor() {
8
+ this.name = "SessionPlugin";
9
+ this.version = "1.0.0";
10
+ this.sessionId = null;
11
+ this.tabId = null;
12
+ this.logger = getLogger();
13
+ }
14
+ init(context) {
15
+ this.context = context;
16
+ this.startSession();
17
+ }
18
+ processEvent(event) {
19
+ if (!this.sessionId) {
20
+ this.startSession();
21
+ }
22
+ event.sessionId = this.sessionId ?? undefined;
23
+ this.extendSession();
24
+ }
25
+ startSession() {
26
+ this.tabId = this.getTabId();
27
+ this.sessionId = generateId();
28
+ this.storeSession();
29
+ this.logger.debug("Session started:", this.sessionId);
30
+ }
31
+ getTabId() {
32
+ if (typeof sessionStorage === "undefined") {
33
+ return generateId();
34
+ }
35
+ let tabId = sessionStorage.getItem(TAB_KEY);
36
+ if (!tabId) {
37
+ tabId = `${generateId()}-${Date.now()}`;
38
+ sessionStorage.setItem(TAB_KEY, tabId);
39
+ }
40
+ return tabId;
41
+ }
42
+ storeSession() {
43
+ if (!this.context || !this.sessionId || !this.tabId) {
44
+ return;
45
+ }
46
+ const expirationDate = new Date();
47
+ expirationDate.setMinutes(expirationDate.getMinutes() + SESSION_EXPIRATION_MINUTES);
48
+ const cookieName = `${SESSION_KEY}_${this.tabId}`;
49
+ this.context.storage.setItem(cookieName, this.sessionId, {
50
+ expires: expirationDate,
51
+ secure: true,
52
+ sameSite: "lax",
53
+ });
54
+ }
55
+ extendSession() {
56
+ if (!this.sessionId) {
57
+ return;
58
+ }
59
+ this.storeSession();
60
+ }
61
+ getSessionId() {
62
+ if (!this.sessionId && this.context) {
63
+ const tabId = this.getTabId();
64
+ const cookieName = `${SESSION_KEY}_${tabId}`;
65
+ this.sessionId = this.context.storage.getItem(cookieName);
66
+ }
67
+ return this.sessionId;
68
+ }
69
+ terminateSession() {
70
+ if (!this.context || !this.tabId) {
71
+ return;
72
+ }
73
+ const cookieName = `${SESSION_KEY}_${this.tabId}`;
74
+ this.context.storage.removeItem(cookieName);
75
+ this.sessionId = null;
76
+ this.logger.debug("Session terminated");
77
+ }
78
+ destroy() {
79
+ this.terminateSession();
80
+ }
81
+ }
@@ -0,0 +1,20 @@
1
+ import type { Plugin, PluginContext, TrackingEvent, User } from "../../core/types";
2
+ export declare class UserPlugin implements Plugin {
3
+ name: string;
4
+ version: string;
5
+ private context?;
6
+ private user;
7
+ private anonymousId;
8
+ private logger;
9
+ init(context: PluginContext): void;
10
+ processEvent(event: TrackingEvent): void;
11
+ identify(user: User): Promise<void>;
12
+ private generateAnonymousId;
13
+ private loadUser;
14
+ private storeUser;
15
+ getUser(): User | null;
16
+ getUserId(): string | null;
17
+ logout(): void;
18
+ private updateAnonymousEvents;
19
+ destroy(): void;
20
+ }