@armco/analytics 0.2.11 → 0.2.12

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 +48 -0
  2. package/core/analytics.js +311 -0
  3. package/core/errors.d.ts +26 -0
  4. package/core/errors.js +54 -0
  5. package/core/types.d.ts +133 -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 +95 -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 +71 -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 +152 -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 +3 -0
  38. package/utils/config-loader.js +59 -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
@@ -0,0 +1,54 @@
1
+ import type { Plugin, PluginContext, EventData } from "../../core/types";
2
+ export interface HTTPRequestEvent extends EventData {
3
+ method: string;
4
+ path: string;
5
+ query?: Record<string, string | string[]>;
6
+ statusCode?: number;
7
+ duration?: number;
8
+ clientIp?: string;
9
+ userAgent?: string;
10
+ origin?: string;
11
+ referer?: string;
12
+ serverHostname?: string;
13
+ serverName?: string;
14
+ requestId?: string;
15
+ errorMessage?: string;
16
+ }
17
+ export interface HTTPRequestMetadata {
18
+ method: string;
19
+ path: string;
20
+ query?: Record<string, string | string[]>;
21
+ headers?: Record<string, string | string[] | undefined>;
22
+ clientIp?: string;
23
+ serverHostname?: string;
24
+ serverName?: string;
25
+ requestId?: string;
26
+ startTime: number;
27
+ }
28
+ export declare class HTTPRequestTrackingPlugin implements Plugin {
29
+ name: string;
30
+ version: string;
31
+ platform: "node";
32
+ private context?;
33
+ private logger;
34
+ private trackRequests;
35
+ private trackResponses;
36
+ private ignoreRoutes;
37
+ private requestMap;
38
+ constructor(options?: {
39
+ trackRequests?: boolean;
40
+ trackResponses?: boolean;
41
+ ignoreRoutes?: string[];
42
+ });
43
+ init(context: PluginContext): void;
44
+ trackRequestStart(metadata: HTTPRequestMetadata): void;
45
+ trackRequestEnd(requestId: string, statusCode: number, error?: Error): void;
46
+ private extractClientIp;
47
+ private extractUserAgent;
48
+ private extractReferer;
49
+ private detectOrigin;
50
+ private getServerHostname;
51
+ private shouldIgnoreRoute;
52
+ private generateRequestId;
53
+ destroy(): void;
54
+ }
@@ -0,0 +1,158 @@
1
+ import { getLogger } from "../../utils/logging";
2
+ export class HTTPRequestTrackingPlugin {
3
+ constructor(options) {
4
+ this.name = "HTTPRequestTrackingPlugin";
5
+ this.version = "1.0.0";
6
+ this.platform = "node";
7
+ this.logger = getLogger();
8
+ this.trackRequests = true;
9
+ this.trackResponses = true;
10
+ this.ignoreRoutes = [];
11
+ this.requestMap = new Map();
12
+ this.trackRequests = options?.trackRequests ?? true;
13
+ this.trackResponses = options?.trackResponses ?? true;
14
+ this.ignoreRoutes = options?.ignoreRoutes ?? [];
15
+ }
16
+ init(context) {
17
+ this.context = context;
18
+ this.logger.debug("HTTP Request Tracking Plugin initialized");
19
+ }
20
+ trackRequestStart(metadata) {
21
+ if (!this.trackRequests || !this.context) {
22
+ return;
23
+ }
24
+ if (this.shouldIgnoreRoute(metadata.path)) {
25
+ this.logger.debug(`Ignoring route: ${metadata.path}`);
26
+ return;
27
+ }
28
+ const requestId = metadata.requestId || this.generateRequestId();
29
+ this.requestMap.set(requestId, { ...metadata, requestId });
30
+ const event = {
31
+ method: metadata.method,
32
+ path: metadata.path,
33
+ query: metadata.query,
34
+ clientIp: this.extractClientIp(metadata),
35
+ userAgent: this.extractUserAgent(metadata),
36
+ origin: this.detectOrigin(metadata),
37
+ referer: this.extractReferer(metadata),
38
+ serverHostname: metadata.serverHostname || this.getServerHostname(),
39
+ serverName: metadata.serverName,
40
+ requestId,
41
+ };
42
+ this.context.track("HTTP_REQUEST_START", event);
43
+ }
44
+ trackRequestEnd(requestId, statusCode, error) {
45
+ if (!this.trackResponses || !this.context) {
46
+ return;
47
+ }
48
+ const metadata = this.requestMap.get(requestId);
49
+ if (!metadata) {
50
+ this.logger.warn(`No metadata found for request: ${requestId}`);
51
+ return;
52
+ }
53
+ const duration = Date.now() - metadata.startTime;
54
+ const event = {
55
+ method: metadata.method,
56
+ path: metadata.path,
57
+ query: metadata.query,
58
+ statusCode,
59
+ duration,
60
+ clientIp: this.extractClientIp(metadata),
61
+ userAgent: this.extractUserAgent(metadata),
62
+ origin: this.detectOrigin(metadata),
63
+ referer: this.extractReferer(metadata),
64
+ serverHostname: metadata.serverHostname || this.getServerHostname(),
65
+ serverName: metadata.serverName,
66
+ requestId,
67
+ errorMessage: error?.message,
68
+ };
69
+ this.context.track("HTTP_REQUEST_END", event);
70
+ this.requestMap.delete(requestId);
71
+ }
72
+ extractClientIp(metadata) {
73
+ if (metadata.clientIp) {
74
+ return metadata.clientIp;
75
+ }
76
+ const headers = metadata.headers;
77
+ if (!headers) {
78
+ return undefined;
79
+ }
80
+ const ipHeaders = [
81
+ "x-forwarded-for",
82
+ "x-real-ip",
83
+ "cf-connecting-ip",
84
+ "x-client-ip",
85
+ "x-forwarded",
86
+ "forwarded-for",
87
+ "forwarded",
88
+ ];
89
+ for (const header of ipHeaders) {
90
+ const value = headers[header];
91
+ if (value) {
92
+ const ip = Array.isArray(value) ? value[0] : value;
93
+ return ip.split(",")[0].trim();
94
+ }
95
+ }
96
+ return undefined;
97
+ }
98
+ extractUserAgent(metadata) {
99
+ const headers = metadata.headers;
100
+ if (!headers) {
101
+ return undefined;
102
+ }
103
+ const ua = headers["user-agent"];
104
+ return Array.isArray(ua) ? ua[0] : ua;
105
+ }
106
+ extractReferer(metadata) {
107
+ const headers = metadata.headers;
108
+ if (!headers) {
109
+ return undefined;
110
+ }
111
+ const referer = headers["referer"] || headers["referrer"];
112
+ return Array.isArray(referer) ? referer[0] : referer;
113
+ }
114
+ detectOrigin(metadata) {
115
+ const headers = metadata.headers;
116
+ if (!headers) {
117
+ return "unknown";
118
+ }
119
+ const referer = headers["referer"] || headers["referrer"];
120
+ const origin = headers["origin"];
121
+ const userAgent = headers["user-agent"];
122
+ if (referer || origin) {
123
+ return Array.isArray(origin) ? origin[0] : (origin || "frontend");
124
+ }
125
+ const ua = Array.isArray(userAgent) ? userAgent[0] : userAgent;
126
+ if (ua && (ua.includes("Mozilla") || ua.includes("Chrome") || ua.includes("Safari"))) {
127
+ return "frontend";
128
+ }
129
+ return "backend";
130
+ }
131
+ getServerHostname() {
132
+ try {
133
+ if (typeof require !== "undefined") {
134
+ const os = require("os");
135
+ return os.hostname();
136
+ }
137
+ }
138
+ catch {
139
+ }
140
+ return "unknown";
141
+ }
142
+ shouldIgnoreRoute(path) {
143
+ return this.ignoreRoutes.some((pattern) => {
144
+ if (pattern.includes("*")) {
145
+ const regex = new RegExp("^" + pattern.replace(/\*/g, ".*") + "$");
146
+ return regex.test(path);
147
+ }
148
+ return path === pattern;
149
+ });
150
+ }
151
+ generateRequestId() {
152
+ return `req_${Date.now()}_${Math.random().toString(36).substring(7)}`;
153
+ }
154
+ destroy() {
155
+ this.requestMap.clear();
156
+ this.logger.debug("HTTP Request Tracking Plugin destroyed");
157
+ }
158
+ }
@@ -0,0 +1,8 @@
1
+ import type { StorageManager, StorageOptions } from "../core/types";
2
+ export declare class CookieStorage implements StorageManager {
3
+ private logger;
4
+ getItem(key: string): string | null;
5
+ setItem(key: string, value: string, options?: StorageOptions): void;
6
+ removeItem(key: string): void;
7
+ clear(): void;
8
+ }
@@ -0,0 +1,60 @@
1
+ import Cookies from "js-cookie";
2
+ import { StorageError } from "../core/errors";
3
+ import { getLogger } from "../utils/logging";
4
+ export class CookieStorage {
5
+ constructor() {
6
+ this.logger = getLogger();
7
+ }
8
+ getItem(key) {
9
+ try {
10
+ const value = Cookies.get(key);
11
+ return value !== undefined ? value : null;
12
+ }
13
+ catch (error) {
14
+ this.logger.error(`Failed to get cookie: ${key}`, error);
15
+ throw new StorageError(`Failed to get cookie: ${key}`);
16
+ }
17
+ }
18
+ setItem(key, value, options) {
19
+ try {
20
+ const cookieOptions = {};
21
+ if (options?.expires) {
22
+ cookieOptions.expires = options.expires;
23
+ }
24
+ if (options?.secure) {
25
+ cookieOptions.secure = true;
26
+ }
27
+ if (options?.sameSite) {
28
+ cookieOptions.sameSite = options.sameSite;
29
+ }
30
+ Cookies.set(key, value, cookieOptions);
31
+ }
32
+ catch (error) {
33
+ this.logger.error(`Failed to set cookie: ${key}`, error);
34
+ throw new StorageError(`Failed to set cookie: ${key}`);
35
+ }
36
+ }
37
+ removeItem(key) {
38
+ try {
39
+ Cookies.remove(key);
40
+ }
41
+ catch (error) {
42
+ this.logger.error(`Failed to remove cookie: ${key}`, error);
43
+ throw new StorageError(`Failed to remove cookie: ${key}`);
44
+ }
45
+ }
46
+ clear() {
47
+ try {
48
+ const cookies = document.cookie.split(";");
49
+ for (const cookie of cookies) {
50
+ const eqPos = cookie.indexOf("=");
51
+ const name = eqPos > -1 ? cookie.slice(0, eqPos) : cookie;
52
+ Cookies.remove(name.trim());
53
+ }
54
+ }
55
+ catch (error) {
56
+ this.logger.error("Failed to clear cookies", error);
57
+ throw new StorageError("Failed to clear cookies");
58
+ }
59
+ }
60
+ }
@@ -0,0 +1,12 @@
1
+ import type { StorageManager, StorageOptions } from "../core/types";
2
+ export declare class HybridStorage implements StorageManager {
3
+ private primary;
4
+ private fallback;
5
+ private logger;
6
+ constructor();
7
+ getItem(key: string): string | null;
8
+ setItem(key: string, value: string, options?: StorageOptions): void;
9
+ removeItem(key: string): void;
10
+ clear(): void;
11
+ isAvailable(): boolean;
12
+ }
@@ -0,0 +1,108 @@
1
+ import { CookieStorage } from "./cookie-storage";
2
+ import { LocalStorage } from "./local-storage";
3
+ import { areCookiesAvailable, isLocalStorageAvailable } from "../utils/helpers";
4
+ import { getLogger } from "../utils/logging";
5
+ export class HybridStorage {
6
+ constructor() {
7
+ this.primary = null;
8
+ this.fallback = null;
9
+ this.logger = getLogger();
10
+ if (areCookiesAvailable()) {
11
+ this.primary = new CookieStorage();
12
+ this.logger.debug("Using cookies as primary storage");
13
+ }
14
+ if (isLocalStorageAvailable()) {
15
+ if (this.primary) {
16
+ this.fallback = new LocalStorage();
17
+ this.logger.debug("Using localStorage as fallback storage");
18
+ }
19
+ else {
20
+ this.primary = new LocalStorage();
21
+ this.logger.debug("Using localStorage as primary storage");
22
+ }
23
+ }
24
+ if (!this.primary) {
25
+ this.logger.warn("No storage mechanism available");
26
+ }
27
+ }
28
+ getItem(key) {
29
+ if (!this.primary) {
30
+ return null;
31
+ }
32
+ try {
33
+ return this.primary.getItem(key);
34
+ }
35
+ catch (error) {
36
+ this.logger.warn("Primary storage failed, trying fallback", error);
37
+ if (this.fallback) {
38
+ try {
39
+ return this.fallback.getItem(key);
40
+ }
41
+ catch (fallbackError) {
42
+ this.logger.error("Both storage mechanisms failed", fallbackError);
43
+ return null;
44
+ }
45
+ }
46
+ return null;
47
+ }
48
+ }
49
+ setItem(key, value, options) {
50
+ if (!this.primary) {
51
+ return;
52
+ }
53
+ try {
54
+ this.primary.setItem(key, value, options);
55
+ }
56
+ catch (error) {
57
+ this.logger.warn("Primary storage failed, trying fallback", error);
58
+ if (this.fallback) {
59
+ try {
60
+ this.fallback.setItem(key, value, options);
61
+ }
62
+ catch (fallbackError) {
63
+ this.logger.error("Both storage mechanisms failed", fallbackError);
64
+ }
65
+ }
66
+ }
67
+ }
68
+ removeItem(key) {
69
+ if (!this.primary) {
70
+ return;
71
+ }
72
+ try {
73
+ this.primary.removeItem(key);
74
+ }
75
+ catch (error) {
76
+ this.logger.warn("Primary storage failed, trying fallback", error);
77
+ }
78
+ if (this.fallback) {
79
+ try {
80
+ this.fallback.removeItem(key);
81
+ }
82
+ catch (fallbackError) {
83
+ this.logger.error("Fallback storage failed", fallbackError);
84
+ }
85
+ }
86
+ }
87
+ clear() {
88
+ if (this.primary) {
89
+ try {
90
+ this.primary.clear();
91
+ }
92
+ catch (error) {
93
+ this.logger.error("Failed to clear primary storage", error);
94
+ }
95
+ }
96
+ if (this.fallback) {
97
+ try {
98
+ this.fallback.clear();
99
+ }
100
+ catch (error) {
101
+ this.logger.error("Failed to clear fallback storage", error);
102
+ }
103
+ }
104
+ }
105
+ isAvailable() {
106
+ return this.primary !== null;
107
+ }
108
+ }
@@ -0,0 +1,8 @@
1
+ import type { StorageManager, StorageOptions } from "../core/types";
2
+ export declare class LocalStorage implements StorageManager {
3
+ private logger;
4
+ getItem(key: string): string | null;
5
+ setItem(key: string, value: string, options?: StorageOptions): void;
6
+ removeItem(key: string): void;
7
+ clear(): void;
8
+ }
@@ -0,0 +1,65 @@
1
+ import { StorageError } from "../core/errors";
2
+ import { getLogger } from "../utils/logging";
3
+ export class LocalStorage {
4
+ constructor() {
5
+ this.logger = getLogger();
6
+ }
7
+ getItem(key) {
8
+ try {
9
+ const stored = localStorage.getItem(key);
10
+ if (!stored) {
11
+ return null;
12
+ }
13
+ try {
14
+ const parsed = JSON.parse(stored);
15
+ if (parsed.expires && Date.now() > parsed.expires) {
16
+ this.removeItem(key);
17
+ return null;
18
+ }
19
+ return parsed.value;
20
+ }
21
+ catch {
22
+ return stored;
23
+ }
24
+ }
25
+ catch (error) {
26
+ this.logger.error(`Failed to get from localStorage: ${key}`, error);
27
+ throw new StorageError(`Failed to get from localStorage: ${key}`);
28
+ }
29
+ }
30
+ setItem(key, value, options) {
31
+ try {
32
+ let toStore = value;
33
+ if (options?.expires) {
34
+ const storageValue = {
35
+ value,
36
+ expires: options.expires.getTime(),
37
+ };
38
+ toStore = JSON.stringify(storageValue);
39
+ }
40
+ localStorage.setItem(key, toStore);
41
+ }
42
+ catch (error) {
43
+ this.logger.error(`Failed to set in localStorage: ${key}`, error);
44
+ throw new StorageError(`Failed to set in localStorage: ${key}`);
45
+ }
46
+ }
47
+ removeItem(key) {
48
+ try {
49
+ localStorage.removeItem(key);
50
+ }
51
+ catch (error) {
52
+ this.logger.error(`Failed to remove from localStorage: ${key}`, error);
53
+ throw new StorageError(`Failed to remove from localStorage: ${key}`);
54
+ }
55
+ }
56
+ clear() {
57
+ try {
58
+ localStorage.clear();
59
+ }
60
+ catch (error) {
61
+ this.logger.error("Failed to clear localStorage", error);
62
+ throw new StorageError("Failed to clear localStorage");
63
+ }
64
+ }
65
+ }
@@ -0,0 +1,9 @@
1
+ import type { StorageManager, StorageOptions } from "../core/types";
2
+ export declare class MemoryStorage implements StorageManager {
3
+ private store;
4
+ private logger;
5
+ getItem(key: string): string | null;
6
+ setItem(key: string, value: string, _options?: StorageOptions): void;
7
+ removeItem(key: string): void;
8
+ clear(): void;
9
+ }
@@ -0,0 +1,22 @@
1
+ import { getLogger } from "../utils/logging";
2
+ export class MemoryStorage {
3
+ constructor() {
4
+ this.store = new Map();
5
+ this.logger = getLogger();
6
+ }
7
+ getItem(key) {
8
+ return this.store.get(key) ?? null;
9
+ }
10
+ setItem(key, value, _options) {
11
+ this.store.set(key, value);
12
+ }
13
+ removeItem(key) {
14
+ this.store.delete(key);
15
+ }
16
+ clear() {
17
+ if (this.store.size > 0) {
18
+ this.logger.debug("Clearing in-memory analytics storage");
19
+ }
20
+ this.store.clear();
21
+ }
22
+ }
@@ -0,0 +1,16 @@
1
+ import type { Transport, TransportResponse, TrackingEvent } from "../core/types";
2
+ export interface BeaconTransportOptions {
3
+ apiKey?: string;
4
+ }
5
+ export declare class BeaconTransport implements Transport {
6
+ private options;
7
+ private logger;
8
+ constructor(options?: BeaconTransportOptions);
9
+ send(endpoint: string, event: TrackingEvent): Promise<TransportResponse>;
10
+ sendBatch(endpoint: string, events: TrackingEvent[]): Promise<TransportResponse>;
11
+ update(endpoint: string, payload: {
12
+ email: string;
13
+ anonymousId: string;
14
+ }): Promise<TransportResponse>;
15
+ private sendBeacon;
16
+ }
@@ -0,0 +1,50 @@
1
+ import { NetworkError } from "../core/errors";
2
+ import { getLogger } from "../utils/logging";
3
+ export class BeaconTransport {
4
+ constructor(options = {}) {
5
+ this.logger = getLogger();
6
+ this.options = options;
7
+ }
8
+ async send(endpoint, event) {
9
+ return this.sendBeacon(endpoint, { event });
10
+ }
11
+ async sendBatch(endpoint, events) {
12
+ return this.sendBeacon(endpoint, { events });
13
+ }
14
+ async update(endpoint, payload) {
15
+ this.logger.warn("BeaconTransport does not support update operations. Use FetchTransport for user identification updates.");
16
+ return {
17
+ success: false,
18
+ error: "Update not supported by BeaconTransport",
19
+ };
20
+ }
21
+ async sendBeacon(endpoint, payload) {
22
+ if (!navigator.sendBeacon) {
23
+ this.logger.warn("Beacon API not available, data may be lost");
24
+ throw new NetworkError("Beacon API not available", undefined, endpoint);
25
+ }
26
+ try {
27
+ const blob = new Blob([JSON.stringify(payload)], {
28
+ type: "application/json",
29
+ });
30
+ const success = navigator.sendBeacon(endpoint, blob);
31
+ if (success) {
32
+ this.logger.debug(`Successfully queued beacon to ${endpoint}`);
33
+ return {
34
+ success: true,
35
+ };
36
+ }
37
+ else {
38
+ this.logger.warn(`Failed to queue beacon to ${endpoint}`);
39
+ return {
40
+ success: false,
41
+ error: "Beacon queue full or rejected",
42
+ };
43
+ }
44
+ }
45
+ catch (error) {
46
+ this.logger.error(`Error sending beacon to ${endpoint}:`, error);
47
+ throw new NetworkError(`Failed to send beacon: ${error}`, undefined, endpoint);
48
+ }
49
+ }
50
+ }
@@ -0,0 +1,20 @@
1
+ import type { Transport, TransportResponse, TrackingEvent } from "../core/types";
2
+ export interface FetchTransportOptions {
3
+ apiKey?: string;
4
+ timeout?: number;
5
+ maxRetries?: number;
6
+ retryDelay?: number;
7
+ }
8
+ export declare class FetchTransport implements Transport {
9
+ private options;
10
+ private logger;
11
+ constructor(options?: FetchTransportOptions);
12
+ send(endpoint: string, event: TrackingEvent): Promise<TransportResponse>;
13
+ sendBatch(endpoint: string, events: TrackingEvent[]): Promise<TransportResponse>;
14
+ private sendWithRetry;
15
+ update(endpoint: string, payload: {
16
+ email: string;
17
+ anonymousId: string;
18
+ }): Promise<TransportResponse>;
19
+ private delay;
20
+ }