@armco/analytics 0.2.10 → 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.
- package/core/analytics.d.ts +48 -0
- package/core/analytics.js +311 -0
- package/core/errors.d.ts +26 -0
- package/core/errors.js +54 -0
- package/core/types.d.ts +133 -0
- package/index.d.ts +24 -2
- package/index.js +24 -13
- package/package.json +9 -28
- package/plugins/auto-track/click.d.ts +15 -0
- package/plugins/auto-track/click.js +95 -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 +71 -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 +152 -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 +3 -0
- package/utils/config-loader.js +59 -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
- package/.npmignore +0 -6
- package/analytics.d.ts +0 -19
- package/analytics.js +0 -537
- package/constants.d.ts +0 -3
- package/constants.js +0 -3
- package/flush.d.ts +0 -3
- package/flush.js +0 -36
- package/helper.d.ts +0 -1
- package/helper.js +0 -3
- package/index.interface.d.ts +0 -28
- package/location.d.ts +0 -6
- package/location.js +0 -42
- package/session.d.ts +0 -4
- package/session.js +0 -76
- package/tsconfig.prod.tsbuildinfo +0 -1
- /package/{index.interface.js → core/types.js} +0 -0
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { NetworkError } from "../core/errors";
|
|
2
|
+
import { getLogger } from "../utils/logging";
|
|
3
|
+
export class FetchTransport {
|
|
4
|
+
constructor(options = {}) {
|
|
5
|
+
this.logger = getLogger();
|
|
6
|
+
this.options = {
|
|
7
|
+
timeout: 5000,
|
|
8
|
+
maxRetries: 3,
|
|
9
|
+
retryDelay: 1000,
|
|
10
|
+
...options,
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
async send(endpoint, event) {
|
|
14
|
+
return this.sendWithRetry(endpoint, { event }, 0);
|
|
15
|
+
}
|
|
16
|
+
async sendBatch(endpoint, events) {
|
|
17
|
+
return this.sendWithRetry(endpoint, { events }, 0);
|
|
18
|
+
}
|
|
19
|
+
async sendWithRetry(endpoint, payload, attempt) {
|
|
20
|
+
try {
|
|
21
|
+
const controller = new AbortController();
|
|
22
|
+
const timeoutId = setTimeout(() => controller.abort(), this.options.timeout);
|
|
23
|
+
const headers = {
|
|
24
|
+
"Content-Type": "application/json",
|
|
25
|
+
};
|
|
26
|
+
if (this.options.apiKey) {
|
|
27
|
+
headers["Authorization"] = `Bearer ${this.options.apiKey}`;
|
|
28
|
+
}
|
|
29
|
+
const response = await fetch(endpoint, {
|
|
30
|
+
method: "POST",
|
|
31
|
+
headers,
|
|
32
|
+
body: JSON.stringify(payload),
|
|
33
|
+
signal: controller.signal,
|
|
34
|
+
});
|
|
35
|
+
clearTimeout(timeoutId);
|
|
36
|
+
if (response.ok) {
|
|
37
|
+
this.logger.debug(`Successfully sent data to ${endpoint}`);
|
|
38
|
+
return {
|
|
39
|
+
success: true,
|
|
40
|
+
statusCode: response.status,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
const errorText = await response.text().catch(() => "Unknown error");
|
|
45
|
+
this.logger.warn(`Failed to send data to ${endpoint}: ${response.status} ${errorText}`);
|
|
46
|
+
if (response.status >= 500 &&
|
|
47
|
+
attempt < (this.options.maxRetries ?? 3)) {
|
|
48
|
+
await this.delay(this.options.retryDelay ?? 1000);
|
|
49
|
+
return this.sendWithRetry(endpoint, payload, attempt + 1);
|
|
50
|
+
}
|
|
51
|
+
return {
|
|
52
|
+
success: false,
|
|
53
|
+
statusCode: response.status,
|
|
54
|
+
error: errorText,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
this.logger.error(`Network error sending to ${endpoint}:`, error);
|
|
60
|
+
if (attempt < (this.options.maxRetries ?? 3)) {
|
|
61
|
+
await this.delay(this.options.retryDelay ?? 1000);
|
|
62
|
+
return this.sendWithRetry(endpoint, payload, attempt + 1);
|
|
63
|
+
}
|
|
64
|
+
throw new NetworkError(`Failed to send data after ${attempt + 1} attempts`, undefined, endpoint);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
async update(endpoint, payload) {
|
|
68
|
+
try {
|
|
69
|
+
const controller = new AbortController();
|
|
70
|
+
const timeoutId = setTimeout(() => controller.abort(), this.options.timeout);
|
|
71
|
+
const headers = {
|
|
72
|
+
"Content-Type": "application/json",
|
|
73
|
+
};
|
|
74
|
+
if (this.options.apiKey) {
|
|
75
|
+
headers["Authorization"] = `Bearer ${this.options.apiKey}`;
|
|
76
|
+
}
|
|
77
|
+
const response = await fetch(endpoint, {
|
|
78
|
+
method: "POST",
|
|
79
|
+
headers,
|
|
80
|
+
body: JSON.stringify(payload),
|
|
81
|
+
signal: controller.signal,
|
|
82
|
+
});
|
|
83
|
+
clearTimeout(timeoutId);
|
|
84
|
+
if (response.ok) {
|
|
85
|
+
this.logger.debug(`Successfully updated events for ${payload.email}`);
|
|
86
|
+
return {
|
|
87
|
+
success: true,
|
|
88
|
+
statusCode: response.status,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
const errorText = await response.text().catch(() => "Unknown error");
|
|
93
|
+
this.logger.warn(`Failed to update events: ${response.status} ${errorText}`);
|
|
94
|
+
return {
|
|
95
|
+
success: false,
|
|
96
|
+
statusCode: response.status,
|
|
97
|
+
error: errorText,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
this.logger.error(`Network error updating events:`, error);
|
|
103
|
+
return {
|
|
104
|
+
success: false,
|
|
105
|
+
error: error instanceof Error ? error.message : String(error),
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
delay(ms) {
|
|
110
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
111
|
+
}
|
|
112
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { getEnvironmentType } from "./helpers";
|
|
2
|
+
import { getLogger } from "./logging";
|
|
3
|
+
const CONFIG_FILE_NAME = "analyticsrc";
|
|
4
|
+
const logger = getLogger();
|
|
5
|
+
export async function loadConfigFromFile() {
|
|
6
|
+
const envType = getEnvironmentType();
|
|
7
|
+
if (envType !== "node") {
|
|
8
|
+
logger.warn("Config file loading is only supported in Node.js environment");
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
try {
|
|
12
|
+
const fs = await import("fs");
|
|
13
|
+
const path = await import("path");
|
|
14
|
+
const projectRoot = process.cwd();
|
|
15
|
+
const configPaths = [
|
|
16
|
+
path.resolve(projectRoot, `${CONFIG_FILE_NAME}.json`),
|
|
17
|
+
path.resolve(projectRoot, `${CONFIG_FILE_NAME}.ts`),
|
|
18
|
+
path.resolve(projectRoot, `${CONFIG_FILE_NAME}.js`),
|
|
19
|
+
];
|
|
20
|
+
for (const configPath of configPaths) {
|
|
21
|
+
if (fs.existsSync(configPath)) {
|
|
22
|
+
logger.debug(`Loading config from: ${configPath}`);
|
|
23
|
+
try {
|
|
24
|
+
if (configPath.endsWith(".json")) {
|
|
25
|
+
const content = fs.readFileSync(configPath, "utf-8");
|
|
26
|
+
const config = JSON.parse(content);
|
|
27
|
+
logger.info(`Successfully loaded config from ${configPath}`);
|
|
28
|
+
return config;
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
const config = await import(configPath);
|
|
32
|
+
logger.info(`Successfully loaded config from ${configPath}`);
|
|
33
|
+
return config.default || config;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
logger.error(`Failed to parse config from ${configPath}:`, error);
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
logger.warn(`No ${CONFIG_FILE_NAME} file found in project root: ${projectRoot}`);
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
logger.error("Failed to load config from file:", error);
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
export async function loadConfig(overrides) {
|
|
51
|
+
const fileConfig = await loadConfigFromFile();
|
|
52
|
+
if (!fileConfig && !overrides) {
|
|
53
|
+
throw new Error("No configuration provided. Either create an analyticsrc.json file or provide config programmatically.");
|
|
54
|
+
}
|
|
55
|
+
return {
|
|
56
|
+
...fileConfig,
|
|
57
|
+
...overrides,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Environment } from "../core/types";
|
|
2
|
+
export declare function generateId(): string;
|
|
3
|
+
export declare function getEnvironmentType(): Environment;
|
|
4
|
+
export declare function getEnvironment(): string;
|
|
5
|
+
export declare function isDoNotTrackEnabled(): boolean;
|
|
6
|
+
export declare function isBrowser(): boolean;
|
|
7
|
+
export declare function areCookiesAvailable(): boolean;
|
|
8
|
+
export declare function isLocalStorageAvailable(): boolean;
|
|
9
|
+
export declare function debounce<T extends (...args: unknown[]) => void>(func: T, wait: number): (...args: Parameters<T>) => void;
|
|
10
|
+
export declare function throttle<T extends (...args: unknown[]) => void>(func: T, limit: number): (...args: Parameters<T>) => void;
|
|
11
|
+
export declare function deepClone<T>(obj: T): T;
|
|
12
|
+
export declare function isPlainObject(value: unknown): value is Record<string, unknown>;
|
|
13
|
+
export declare function deepMerge<T extends Record<string, unknown>>(target: T, ...sources: Partial<T>[]): T;
|
|
14
|
+
export declare function getTimestamp(): Date;
|
|
15
|
+
export declare function isArmcoClient(name: string | null): boolean;
|
package/utils/helpers.js
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { v4 as uuidv4 } from "uuid";
|
|
2
|
+
export function generateId() {
|
|
3
|
+
return uuidv4();
|
|
4
|
+
}
|
|
5
|
+
export function getEnvironmentType() {
|
|
6
|
+
if (typeof window !== "undefined" && typeof window.document !== "undefined") {
|
|
7
|
+
return "browser";
|
|
8
|
+
}
|
|
9
|
+
else if (typeof process !== "undefined" &&
|
|
10
|
+
process.versions != null &&
|
|
11
|
+
process.versions.node != null) {
|
|
12
|
+
return "node";
|
|
13
|
+
}
|
|
14
|
+
else {
|
|
15
|
+
return "unknown";
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
export function getEnvironment() {
|
|
19
|
+
if (typeof process !== "undefined" && process.env && process.env.NODE_ENV) {
|
|
20
|
+
return process.env.NODE_ENV;
|
|
21
|
+
}
|
|
22
|
+
try {
|
|
23
|
+
const getImportMeta = new Function('return typeof import.meta !== "undefined" ? import.meta : null');
|
|
24
|
+
const meta = getImportMeta();
|
|
25
|
+
if (meta && meta.env && meta.env.MODE) {
|
|
26
|
+
return meta.env.MODE;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
catch (e) {
|
|
30
|
+
}
|
|
31
|
+
return "development";
|
|
32
|
+
}
|
|
33
|
+
export function isDoNotTrackEnabled() {
|
|
34
|
+
if (typeof navigator !== "undefined" && "doNotTrack" in navigator) {
|
|
35
|
+
const doNotTrackValue = navigator.doNotTrack;
|
|
36
|
+
return doNotTrackValue === "1" || doNotTrackValue === "yes";
|
|
37
|
+
}
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
export function isBrowser() {
|
|
41
|
+
return getEnvironmentType() === "browser";
|
|
42
|
+
}
|
|
43
|
+
export function areCookiesAvailable() {
|
|
44
|
+
if (!isBrowser()) {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
try {
|
|
48
|
+
document.cookie = "test=1";
|
|
49
|
+
const result = document.cookie.indexOf("test=") !== -1;
|
|
50
|
+
document.cookie = "test=1; expires=Thu, 01-Jan-1970 00:00:01 GMT";
|
|
51
|
+
return result;
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
export function isLocalStorageAvailable() {
|
|
58
|
+
if (!isBrowser()) {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
try {
|
|
62
|
+
const test = "__storage_test__";
|
|
63
|
+
localStorage.setItem(test, test);
|
|
64
|
+
localStorage.removeItem(test);
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
export function debounce(func, wait) {
|
|
72
|
+
let timeout = null;
|
|
73
|
+
return function (...args) {
|
|
74
|
+
const later = () => {
|
|
75
|
+
timeout = null;
|
|
76
|
+
func.apply(this, args);
|
|
77
|
+
};
|
|
78
|
+
if (timeout !== null) {
|
|
79
|
+
clearTimeout(timeout);
|
|
80
|
+
}
|
|
81
|
+
timeout = setTimeout(later, wait);
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
export function throttle(func, limit) {
|
|
85
|
+
let inThrottle;
|
|
86
|
+
return function (...args) {
|
|
87
|
+
if (!inThrottle) {
|
|
88
|
+
func.apply(this, args);
|
|
89
|
+
inThrottle = true;
|
|
90
|
+
setTimeout(() => (inThrottle = false), limit);
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
export function deepClone(obj) {
|
|
95
|
+
if (obj === null || typeof obj !== "object") {
|
|
96
|
+
return obj;
|
|
97
|
+
}
|
|
98
|
+
if (obj instanceof Date) {
|
|
99
|
+
return new Date(obj.getTime());
|
|
100
|
+
}
|
|
101
|
+
if (obj instanceof Array) {
|
|
102
|
+
return obj.map((item) => deepClone(item));
|
|
103
|
+
}
|
|
104
|
+
if (obj instanceof Object) {
|
|
105
|
+
const cloned = {};
|
|
106
|
+
for (const key in obj) {
|
|
107
|
+
if (Object.prototype.hasOwnProperty.call(obj, key)) {
|
|
108
|
+
cloned[key] = deepClone(obj[key]);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return cloned;
|
|
112
|
+
}
|
|
113
|
+
return obj;
|
|
114
|
+
}
|
|
115
|
+
export function isPlainObject(value) {
|
|
116
|
+
return (typeof value === "object" &&
|
|
117
|
+
value !== null &&
|
|
118
|
+
value.constructor === Object &&
|
|
119
|
+
Object.prototype.toString.call(value) === "[object Object]");
|
|
120
|
+
}
|
|
121
|
+
export function deepMerge(target, ...sources) {
|
|
122
|
+
if (!sources.length) {
|
|
123
|
+
return target;
|
|
124
|
+
}
|
|
125
|
+
const source = sources.shift();
|
|
126
|
+
if (!source) {
|
|
127
|
+
return deepMerge(target, ...sources);
|
|
128
|
+
}
|
|
129
|
+
for (const key in source) {
|
|
130
|
+
if (Object.prototype.hasOwnProperty.call(source, key)) {
|
|
131
|
+
const sourceValue = source[key];
|
|
132
|
+
const targetValue = target[key];
|
|
133
|
+
if (isPlainObject(sourceValue) && isPlainObject(targetValue)) {
|
|
134
|
+
target[key] = deepMerge({ ...targetValue }, sourceValue);
|
|
135
|
+
}
|
|
136
|
+
else if (sourceValue !== undefined) {
|
|
137
|
+
target[key] = sourceValue;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return deepMerge(target, ...sources);
|
|
142
|
+
}
|
|
143
|
+
export function getTimestamp() {
|
|
144
|
+
return new Date();
|
|
145
|
+
}
|
|
146
|
+
export function isArmcoClient(name) {
|
|
147
|
+
return !!(name && name.startsWith("@armco"));
|
|
148
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { LogLevel } from "../core/types";
|
|
2
|
+
export declare class Logger {
|
|
3
|
+
private level;
|
|
4
|
+
private prefix;
|
|
5
|
+
constructor(level?: LogLevel, prefix?: string);
|
|
6
|
+
setLevel(level: LogLevel): void;
|
|
7
|
+
getLevel(): LogLevel;
|
|
8
|
+
debug(message: string, ...args: unknown[]): void;
|
|
9
|
+
info(message: string, ...args: unknown[]): void;
|
|
10
|
+
warn(message: string, ...args: unknown[]): void;
|
|
11
|
+
error(message: string, ...args: unknown[]): void;
|
|
12
|
+
private format;
|
|
13
|
+
private shouldLog;
|
|
14
|
+
}
|
|
15
|
+
export declare function getLogger(): Logger;
|
|
16
|
+
export declare function setLogger(logger: Logger): void;
|
|
17
|
+
export declare function createLogger(level?: LogLevel, prefix?: string): Logger;
|
package/utils/logging.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
export class Logger {
|
|
2
|
+
constructor(level = "info", prefix = "[Analytics]") {
|
|
3
|
+
this.level = level;
|
|
4
|
+
this.prefix = prefix;
|
|
5
|
+
}
|
|
6
|
+
setLevel(level) {
|
|
7
|
+
this.level = level;
|
|
8
|
+
}
|
|
9
|
+
getLevel() {
|
|
10
|
+
return this.level;
|
|
11
|
+
}
|
|
12
|
+
debug(message, ...args) {
|
|
13
|
+
if (this.shouldLog("debug")) {
|
|
14
|
+
console.debug(this.format(message), ...args);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
info(message, ...args) {
|
|
18
|
+
if (this.shouldLog("info")) {
|
|
19
|
+
console.info(this.format(message), ...args);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
warn(message, ...args) {
|
|
23
|
+
if (this.shouldLog("warn")) {
|
|
24
|
+
console.warn(this.format(message), ...args);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
error(message, ...args) {
|
|
28
|
+
if (this.shouldLog("error")) {
|
|
29
|
+
console.error(this.format(message), ...args);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
format(message) {
|
|
33
|
+
return `${this.prefix} ${message}`;
|
|
34
|
+
}
|
|
35
|
+
shouldLog(messageLevel) {
|
|
36
|
+
if (this.level === "none") {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
const levels = ["debug", "info", "warn", "error", "none"];
|
|
40
|
+
const currentLevelIndex = levels.indexOf(this.level);
|
|
41
|
+
const messageLevelIndex = levels.indexOf(messageLevel);
|
|
42
|
+
return messageLevelIndex >= currentLevelIndex;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
let defaultLogger = new Logger();
|
|
46
|
+
export function getLogger() {
|
|
47
|
+
return defaultLogger;
|
|
48
|
+
}
|
|
49
|
+
export function setLogger(logger) {
|
|
50
|
+
defaultLogger = logger;
|
|
51
|
+
}
|
|
52
|
+
export function createLogger(level = "info", prefix = "[Analytics]") {
|
|
53
|
+
return new Logger(level, prefix);
|
|
54
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { AnalyticsConfig, EventData, User, PageViewEvent, ClickEvent, FormEvent, ErrorEvent } from "../core/types";
|
|
2
|
+
export declare function validateConfig(config: unknown): AnalyticsConfig;
|
|
3
|
+
export declare function validateUser(user: unknown): User;
|
|
4
|
+
export declare function validatePageView(data: unknown): PageViewEvent;
|
|
5
|
+
export declare function validateClickEvent(data: unknown): ClickEvent;
|
|
6
|
+
export declare function validateFormEvent(data: unknown): FormEvent;
|
|
7
|
+
export declare function validateErrorEvent(data: unknown): ErrorEvent;
|
|
8
|
+
export declare function validateEventType(eventType: unknown): string;
|
|
9
|
+
export declare function sanitizeEventData(data: EventData): EventData;
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { ValidationError } from "../core/errors";
|
|
3
|
+
const configSchema = z.object({
|
|
4
|
+
apiKey: z.string().optional(),
|
|
5
|
+
endpoint: z.string().url().optional(),
|
|
6
|
+
hostProjectName: z.string().optional(),
|
|
7
|
+
trackEvents: z.array(z.string()).optional(),
|
|
8
|
+
submissionStrategy: z.enum(["ONEVENT", "DEFER"]).optional(),
|
|
9
|
+
showConsentPopup: z.boolean().optional(),
|
|
10
|
+
logLevel: z.enum(["debug", "info", "warn", "error", "none"]).optional(),
|
|
11
|
+
samplingRate: z.number().min(0).max(1).optional(),
|
|
12
|
+
enableLocation: z.boolean().optional(),
|
|
13
|
+
enableAutoTrack: z.boolean().optional(),
|
|
14
|
+
respectDoNotTrack: z.boolean().optional(),
|
|
15
|
+
batchSize: z.number().positive().optional(),
|
|
16
|
+
flushInterval: z.number().positive().optional(),
|
|
17
|
+
maxRetries: z.number().nonnegative().optional(),
|
|
18
|
+
retryDelay: z.number().positive().optional(),
|
|
19
|
+
});
|
|
20
|
+
const userSchema = z.object({
|
|
21
|
+
email: z.string().email("Invalid email address"),
|
|
22
|
+
id: z.string().optional(),
|
|
23
|
+
name: z.string().optional(),
|
|
24
|
+
}).passthrough();
|
|
25
|
+
const pageViewSchema = z.object({
|
|
26
|
+
pageName: z.string().min(1, "Page name is required"),
|
|
27
|
+
url: z.string().url("Invalid URL"),
|
|
28
|
+
referrer: z.string().optional(),
|
|
29
|
+
title: z.string().optional(),
|
|
30
|
+
}).passthrough();
|
|
31
|
+
const clickEventSchema = z.object({
|
|
32
|
+
elementType: z.string().min(1, "Element type is required"),
|
|
33
|
+
elementId: z.string().optional(),
|
|
34
|
+
elementText: z.string().optional(),
|
|
35
|
+
elementClasses: z.array(z.string()).optional(),
|
|
36
|
+
elementPath: z.string().optional(),
|
|
37
|
+
href: z.string().optional(),
|
|
38
|
+
value: z.string().optional(),
|
|
39
|
+
}).passthrough();
|
|
40
|
+
const formEventSchema = z.object({
|
|
41
|
+
formId: z.string().optional(),
|
|
42
|
+
formName: z.string().optional(),
|
|
43
|
+
formAction: z.string().optional(),
|
|
44
|
+
formMethod: z.string().optional(),
|
|
45
|
+
}).passthrough();
|
|
46
|
+
const errorEventSchema = z.object({
|
|
47
|
+
errorMessage: z.string().min(1, "Error message is required"),
|
|
48
|
+
errorStack: z.string().optional(),
|
|
49
|
+
errorType: z.string().optional(),
|
|
50
|
+
}).passthrough();
|
|
51
|
+
export function validateConfig(config) {
|
|
52
|
+
try {
|
|
53
|
+
return configSchema.parse(config);
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
if (error instanceof z.ZodError) {
|
|
57
|
+
const messages = error.issues.map((err) => `${err.path.join(".")}: ${err.message}`);
|
|
58
|
+
throw new ValidationError(`Configuration validation failed: ${messages.join(", ")}`);
|
|
59
|
+
}
|
|
60
|
+
throw error;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
export function validateUser(user) {
|
|
64
|
+
try {
|
|
65
|
+
return userSchema.parse(user);
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
if (error instanceof z.ZodError) {
|
|
69
|
+
const messages = error.issues.map((err) => `${err.path.join(".")}: ${err.message}`);
|
|
70
|
+
throw new ValidationError(`User validation failed: ${messages.join(", ")}`);
|
|
71
|
+
}
|
|
72
|
+
throw error;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
export function validatePageView(data) {
|
|
76
|
+
try {
|
|
77
|
+
return pageViewSchema.parse(data);
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
if (error instanceof z.ZodError) {
|
|
81
|
+
const messages = error.issues.map((err) => `${err.path.join(".")}: ${err.message}`);
|
|
82
|
+
throw new ValidationError(`Page view validation failed: ${messages.join(", ")}`);
|
|
83
|
+
}
|
|
84
|
+
throw error;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
export function validateClickEvent(data) {
|
|
88
|
+
try {
|
|
89
|
+
return clickEventSchema.parse(data);
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
if (error instanceof z.ZodError) {
|
|
93
|
+
const messages = error.issues.map((err) => `${err.path.join(".")}: ${err.message}`);
|
|
94
|
+
throw new ValidationError(`Click event validation failed: ${messages.join(", ")}`);
|
|
95
|
+
}
|
|
96
|
+
throw error;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
export function validateFormEvent(data) {
|
|
100
|
+
try {
|
|
101
|
+
return formEventSchema.parse(data);
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
if (error instanceof z.ZodError) {
|
|
105
|
+
const messages = error.issues.map((err) => `${err.path.join(".")}: ${err.message}`);
|
|
106
|
+
throw new ValidationError(`Form event validation failed: ${messages.join(", ")}`);
|
|
107
|
+
}
|
|
108
|
+
throw error;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
export function validateErrorEvent(data) {
|
|
112
|
+
try {
|
|
113
|
+
return errorEventSchema.parse(data);
|
|
114
|
+
}
|
|
115
|
+
catch (error) {
|
|
116
|
+
if (error instanceof z.ZodError) {
|
|
117
|
+
const messages = error.issues.map((err) => `${err.path.join(".")}: ${err.message}`);
|
|
118
|
+
throw new ValidationError(`Error event validation failed: ${messages.join(", ")}`);
|
|
119
|
+
}
|
|
120
|
+
throw error;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
export function validateEventType(eventType) {
|
|
124
|
+
if (typeof eventType !== "string" || eventType.trim().length === 0) {
|
|
125
|
+
throw new ValidationError("Event type must be a non-empty string");
|
|
126
|
+
}
|
|
127
|
+
return eventType.trim();
|
|
128
|
+
}
|
|
129
|
+
export function sanitizeEventData(data) {
|
|
130
|
+
const sanitized = {};
|
|
131
|
+
for (const [key, value] of Object.entries(data)) {
|
|
132
|
+
if (typeof value === "function" || value === undefined) {
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
if (typeof value === "string") {
|
|
136
|
+
sanitized[key] = value.trim();
|
|
137
|
+
}
|
|
138
|
+
else if (Array.isArray(value)) {
|
|
139
|
+
sanitized[key] = value.map((item) => typeof item === "string" ? item.trim() : item);
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
sanitized[key] = value;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return sanitized;
|
|
146
|
+
}
|
package/.npmignore
DELETED
package/analytics.d.ts
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import { startSession, getSessionId } from "./session";
|
|
2
|
-
import { User, Event, ConfigType } from "./index.interface";
|
|
3
|
-
declare let enabled: boolean | null;
|
|
4
|
-
declare function loadConfiguration(): Promise<any>;
|
|
5
|
-
declare function getEnvironment(): string;
|
|
6
|
-
declare function getEnvironmentType(): "browser" | "node" | "unknown";
|
|
7
|
-
export declare function sendBulkData(data: Event[], callback?: Function): Promise<void>;
|
|
8
|
-
declare function trackEvent(event: string | Event, data?: any): void;
|
|
9
|
-
declare function trackPageView(pageName: string, data?: {
|
|
10
|
-
[key: string]: any;
|
|
11
|
-
}): void;
|
|
12
|
-
declare function trackError(errorMessage: string): void;
|
|
13
|
-
declare function enableTracking(enable: boolean): void;
|
|
14
|
-
declare function generateAnonymousId(): string;
|
|
15
|
-
declare function identify(appUser: User): void;
|
|
16
|
-
declare function sendHostProjectName(): void;
|
|
17
|
-
declare function init(config?: ConfigType): void;
|
|
18
|
-
declare function logout(): void;
|
|
19
|
-
export { enabled as analyticsEnabled, init, identify, getEnvironmentType, getEnvironment, loadConfiguration, logout, trackEvent, trackPageView, trackError, enableTracking, generateAnonymousId, sendHostProjectName, getSessionId, startSession, };
|