@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.
- package/core/analytics.d.ts +49 -0
- package/core/analytics.js +354 -0
- package/core/errors.d.ts +26 -0
- package/core/errors.js +54 -0
- package/core/types.d.ts +137 -0
- package/core/types.js +1 -0
- package/global-modules.d.ts +14 -0
- package/index.d.ts +24 -0
- package/index.js +24 -0
- package/package.json +6 -36
- package/plugins/auto-track/click.d.ts +15 -0
- package/plugins/auto-track/click.js +99 -0
- package/plugins/auto-track/error.d.ts +15 -0
- package/plugins/auto-track/error.js +65 -0
- package/plugins/auto-track/form.d.ts +13 -0
- package/plugins/auto-track/form.js +54 -0
- package/plugins/auto-track/page.d.ts +14 -0
- package/plugins/auto-track/page.js +72 -0
- package/plugins/enrichment/session.d.ts +18 -0
- package/plugins/enrichment/session.js +81 -0
- package/plugins/enrichment/user.d.ts +20 -0
- package/plugins/enrichment/user.js +159 -0
- package/plugins/node/http-request-tracking.d.ts +54 -0
- package/plugins/node/http-request-tracking.js +158 -0
- package/storage/cookie-storage.d.ts +8 -0
- package/storage/cookie-storage.js +60 -0
- package/storage/hybrid-storage.d.ts +12 -0
- package/storage/hybrid-storage.js +108 -0
- package/storage/local-storage.d.ts +8 -0
- package/storage/local-storage.js +65 -0
- package/storage/memory-storage.d.ts +9 -0
- package/storage/memory-storage.js +22 -0
- package/transport/beacon-transport.d.ts +16 -0
- package/transport/beacon-transport.js +50 -0
- package/transport/fetch-transport.d.ts +20 -0
- package/transport/fetch-transport.js +112 -0
- package/utils/config-loader.d.ts +6 -0
- package/utils/config-loader.js +168 -0
- package/utils/helpers.d.ts +15 -0
- package/utils/helpers.js +148 -0
- package/utils/logging.d.ts +17 -0
- package/utils/logging.js +54 -0
- package/utils/validation.d.ts +9 -0
- package/utils/validation.js +146 -0
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { generateId, getEnvironmentType } from "../../utils/helpers";
|
|
2
|
+
import { validateUser } from "../../utils/validation";
|
|
3
|
+
import { getLogger } from "../../utils/logging";
|
|
4
|
+
const USER_KEY = "ar_user";
|
|
5
|
+
const ANONYMOUS_ID_KEY = "ar_anonymous_id";
|
|
6
|
+
export class UserPlugin {
|
|
7
|
+
constructor() {
|
|
8
|
+
this.name = "UserPlugin";
|
|
9
|
+
this.version = "1.0.0";
|
|
10
|
+
this.user = null;
|
|
11
|
+
this.anonymousId = null;
|
|
12
|
+
this.logger = getLogger();
|
|
13
|
+
}
|
|
14
|
+
init(context) {
|
|
15
|
+
this.context = context;
|
|
16
|
+
this.loadUser();
|
|
17
|
+
if (!this.user) {
|
|
18
|
+
this.generateAnonymousId();
|
|
19
|
+
}
|
|
20
|
+
const userId = this.getUserId();
|
|
21
|
+
if (userId) {
|
|
22
|
+
this.logger.info(`Tracking User as ${userId}`);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
processEvent(event) {
|
|
26
|
+
if (this.user) {
|
|
27
|
+
event.userId = this.user.email;
|
|
28
|
+
event.data = {
|
|
29
|
+
...event.data,
|
|
30
|
+
user: this.user,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
else if (this.anonymousId) {
|
|
34
|
+
event.userId = this.anonymousId;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
async identify(user) {
|
|
38
|
+
try {
|
|
39
|
+
const validatedUser = validateUser(user);
|
|
40
|
+
this.user = validatedUser;
|
|
41
|
+
this.storeUser(validatedUser);
|
|
42
|
+
if (this.context && this.anonymousId) {
|
|
43
|
+
const previousAnonymousId = this.anonymousId;
|
|
44
|
+
this.context.storage.removeItem(ANONYMOUS_ID_KEY);
|
|
45
|
+
this.context.track("IDENTIFY", {
|
|
46
|
+
anonymousId: previousAnonymousId,
|
|
47
|
+
email: validatedUser.email,
|
|
48
|
+
});
|
|
49
|
+
if (getEnvironmentType() === "browser") {
|
|
50
|
+
await this.updateAnonymousEvents(validatedUser.email, previousAnonymousId);
|
|
51
|
+
}
|
|
52
|
+
this.anonymousId = null;
|
|
53
|
+
}
|
|
54
|
+
this.logger.info("User identified:", validatedUser.email);
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
this.logger.error("Failed to identify user:", error);
|
|
58
|
+
throw error;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
generateAnonymousId() {
|
|
62
|
+
if (!this.context) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
this.anonymousId = this.context.storage.getItem(ANONYMOUS_ID_KEY);
|
|
66
|
+
if (!this.anonymousId) {
|
|
67
|
+
this.anonymousId = generateId();
|
|
68
|
+
this.context.storage.setItem(ANONYMOUS_ID_KEY, this.anonymousId);
|
|
69
|
+
this.logger.debug("Anonymous ID generated:", this.anonymousId);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
loadUser() {
|
|
73
|
+
if (!this.context) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
const storedUser = this.context.storage.getItem(USER_KEY);
|
|
77
|
+
if (storedUser) {
|
|
78
|
+
try {
|
|
79
|
+
this.user = JSON.parse(storedUser);
|
|
80
|
+
this.logger.debug("User loaded from storage:", this.user?.email);
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
this.logger.error("Failed to parse stored user:", error);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
storeUser(user) {
|
|
88
|
+
if (!this.context) {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
try {
|
|
92
|
+
this.context.storage.setItem(USER_KEY, JSON.stringify(user));
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
this.logger.error("Failed to store user:", error);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
getUser() {
|
|
99
|
+
return this.user;
|
|
100
|
+
}
|
|
101
|
+
getUserId() {
|
|
102
|
+
return this.user?.email ?? this.anonymousId;
|
|
103
|
+
}
|
|
104
|
+
logout() {
|
|
105
|
+
if (!this.context) {
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
this.context.storage.removeItem(USER_KEY);
|
|
109
|
+
this.user = null;
|
|
110
|
+
this.generateAnonymousId();
|
|
111
|
+
this.logger.info("User logged out");
|
|
112
|
+
}
|
|
113
|
+
async updateAnonymousEvents(email, anonymousId) {
|
|
114
|
+
if (!this.context) {
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
const { config, transport } = this.context;
|
|
118
|
+
if (!transport.update) {
|
|
119
|
+
this.logger.debug("Transport does not support update, skipping event tagging");
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
let updateEndpoint = config.updateEndpoint;
|
|
123
|
+
if (!updateEndpoint) {
|
|
124
|
+
if (config.apiKey) {
|
|
125
|
+
updateEndpoint = "https://telemetry.armco.dev/events/tag";
|
|
126
|
+
}
|
|
127
|
+
else if (config.resolvedEndpoint) {
|
|
128
|
+
updateEndpoint = config.resolvedEndpoint.replace("/add", "/tag");
|
|
129
|
+
}
|
|
130
|
+
else if (typeof config.endpoint === "string") {
|
|
131
|
+
updateEndpoint = config.endpoint.replace("/add", "/tag");
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
this.logger.warn("No update endpoint configured, skipping event tagging");
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
try {
|
|
139
|
+
this.logger.info(`Updating anonymous events (${anonymousId}) with user identity (${email})`);
|
|
140
|
+
const response = await transport.update(updateEndpoint, {
|
|
141
|
+
email,
|
|
142
|
+
anonymousId,
|
|
143
|
+
});
|
|
144
|
+
if (response.success) {
|
|
145
|
+
this.logger.info(`Successfully tagged ${anonymousId} events with user ${email}`);
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
this.logger.warn(`Failed to tag events: ${response.error || "Unknown error"}`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
catch (error) {
|
|
152
|
+
this.logger.error("Error updating anonymous events:", error);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
destroy() {
|
|
156
|
+
this.user = null;
|
|
157
|
+
this.anonymousId = null;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
@@ -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
|
+
}
|