growlytics-tracking 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.
- package/LICENSE +21 -0
- package/README.md +206 -0
- package/dist/cjs/engagement.d.ts +6 -0
- package/dist/cjs/engagement.js +46 -0
- package/dist/cjs/final_payload.d.ts +13 -0
- package/dist/cjs/final_payload.js +2 -0
- package/dist/cjs/growlytics.d.ts +16 -0
- package/dist/cjs/growlytics.js +55 -0
- package/dist/cjs/helper_types.d.ts +16 -0
- package/dist/cjs/helper_types.js +2 -0
- package/dist/cjs/http.d.ts +35 -0
- package/dist/cjs/http.js +176 -0
- package/dist/cjs/index.d.ts +2 -0
- package/dist/cjs/index.js +20 -0
- package/dist/cjs/package.json +3 -0
- package/dist/cjs/queue.d.ts +45 -0
- package/dist/cjs/queue.js +121 -0
- package/dist/cjs/tracker.d.ts +28 -0
- package/dist/cjs/tracker.js +177 -0
- package/dist/cjs/types.d.ts +16 -0
- package/dist/cjs/types.js +2 -0
- package/dist/cjs/utils.d.ts +15 -0
- package/dist/cjs/utils.js +88 -0
- package/dist/esm/engagement.d.ts +6 -0
- package/dist/esm/engagement.js +46 -0
- package/dist/esm/final_payload.d.ts +13 -0
- package/dist/esm/final_payload.js +2 -0
- package/dist/esm/growlytics.d.ts +16 -0
- package/dist/esm/growlytics.js +55 -0
- package/dist/esm/helper_types.d.ts +16 -0
- package/dist/esm/helper_types.js +2 -0
- package/dist/esm/http.d.ts +35 -0
- package/dist/esm/http.js +176 -0
- package/dist/esm/index.d.ts +2 -0
- package/dist/esm/index.js +20 -0
- package/dist/esm/package.json +3 -0
- package/dist/esm/queue.d.ts +45 -0
- package/dist/esm/queue.js +121 -0
- package/dist/esm/tracker.d.ts +28 -0
- package/dist/esm/tracker.js +177 -0
- package/dist/esm/types.d.ts +16 -0
- package/dist/esm/types.js +2 -0
- package/dist/esm/utils.d.ts +15 -0
- package/dist/esm/utils.js +88 -0
- package/package.json +56 -0
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.EventQueue = void 0;
|
|
4
|
+
class EventQueue {
|
|
5
|
+
client;
|
|
6
|
+
config;
|
|
7
|
+
queue = [];
|
|
8
|
+
timer = null;
|
|
9
|
+
isFlushing = false;
|
|
10
|
+
activeFlushPromise = null;
|
|
11
|
+
constructor(client, config) {
|
|
12
|
+
this.client = client;
|
|
13
|
+
this.config = config;
|
|
14
|
+
this.startTimer();
|
|
15
|
+
}
|
|
16
|
+
log(message, ...args) {
|
|
17
|
+
if (this.config.debug) {
|
|
18
|
+
console.log(`[Growlytics-SDK][Queue] ${message}`, ...args);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Add a tracking event to the queue. If the queue is full, the oldest event is discarded.
|
|
23
|
+
*/
|
|
24
|
+
add(event) {
|
|
25
|
+
if (this.queue.length >= this.config.maxQueueSize) {
|
|
26
|
+
const dropped = this.queue.shift();
|
|
27
|
+
this.log(`Queue full (${this.config.maxQueueSize}). Dropped oldest event to prevent OOM:`, dropped ? dropped.event_type : 'unknown');
|
|
28
|
+
}
|
|
29
|
+
this.queue.push(event);
|
|
30
|
+
this.log(`Event '${event.event_type}' added. Queue size: ${this.queue.length}`);
|
|
31
|
+
// If batch size is reached, trigger an asynchronous flush immediately
|
|
32
|
+
if (this.queue.length >= this.config.batchSize) {
|
|
33
|
+
this.flush().catch(() => { });
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Manually trigger a flush of all currently queued events.
|
|
38
|
+
* Returns a promise that resolves when the flush is complete.
|
|
39
|
+
*/
|
|
40
|
+
async flush() {
|
|
41
|
+
if (this.isFlushing) {
|
|
42
|
+
this.log('Flush already in progress, waiting for it to complete...');
|
|
43
|
+
return this.activeFlushPromise || Promise.resolve();
|
|
44
|
+
}
|
|
45
|
+
this.isFlushing = true;
|
|
46
|
+
this.activeFlushPromise = this.performFlush();
|
|
47
|
+
try {
|
|
48
|
+
await this.activeFlushPromise;
|
|
49
|
+
}
|
|
50
|
+
finally {
|
|
51
|
+
this.isFlushing = false;
|
|
52
|
+
this.activeFlushPromise = null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Performs the flush logic, calling the HttpClient to send batches of events.
|
|
57
|
+
*/
|
|
58
|
+
async performFlush() {
|
|
59
|
+
this.resetTimer();
|
|
60
|
+
while (this.queue.length > 0) {
|
|
61
|
+
// Dequeue a batch of events
|
|
62
|
+
const batch = this.queue.splice(0, this.config.batchSize);
|
|
63
|
+
if (batch.length === 0) {
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
this.log(`Flushing batch of ${batch.length} events...`);
|
|
67
|
+
try {
|
|
68
|
+
await this.client.sendEvents(batch);
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
this.log(`Error flushing batch of ${batch.length} events:`, error.message || error);
|
|
72
|
+
// Trigger error callback for custom handling (e.g. write to fallback log file, emit metric)
|
|
73
|
+
if (this.config.onError) {
|
|
74
|
+
try {
|
|
75
|
+
this.config.onError(error, batch);
|
|
76
|
+
}
|
|
77
|
+
catch (callbackErr) {
|
|
78
|
+
this.log('Error inside user-defined onError handler:', callbackErr);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
startTimer() {
|
|
85
|
+
this.resetTimer();
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Resets the background interval timer.
|
|
89
|
+
*/
|
|
90
|
+
resetTimer() {
|
|
91
|
+
if (this.timer) {
|
|
92
|
+
clearInterval(this.timer);
|
|
93
|
+
}
|
|
94
|
+
this.timer = setInterval(() => {
|
|
95
|
+
if (this.queue.length > 0) {
|
|
96
|
+
this.log(`Flush interval of ${this.config.flushIntervalMs}ms reached. Flushing...`);
|
|
97
|
+
this.flush().catch(() => { });
|
|
98
|
+
}
|
|
99
|
+
}, this.config.flushIntervalMs);
|
|
100
|
+
// Unref the timer so it doesn't block process exit if there is no other work in Node's event loop
|
|
101
|
+
if (this.timer && typeof this.timer.unref === 'function') {
|
|
102
|
+
this.timer.unref();
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Stops the background timer.
|
|
107
|
+
*/
|
|
108
|
+
stop() {
|
|
109
|
+
if (this.timer) {
|
|
110
|
+
clearInterval(this.timer);
|
|
111
|
+
this.timer = null;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Get the current count of events in the queue.
|
|
116
|
+
*/
|
|
117
|
+
get length() {
|
|
118
|
+
return this.queue.length;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
exports.EventQueue = EventQueue;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { GrowlyticsConfig, TrackingEventPayload } from './types';
|
|
2
|
+
export declare class GrowlyticsTracker {
|
|
3
|
+
private config;
|
|
4
|
+
private client;
|
|
5
|
+
private queue;
|
|
6
|
+
constructor(options?: GrowlyticsConfig);
|
|
7
|
+
private log;
|
|
8
|
+
/**
|
|
9
|
+
* Tracks an event by compiling client metadata and placing it in the buffer queue.
|
|
10
|
+
*
|
|
11
|
+
* @param eventType Name of the event, e.g. 'product_view' or 'add_to_cart'
|
|
12
|
+
* @param payload Custom event details and parameters
|
|
13
|
+
*/
|
|
14
|
+
track(eventType: string, payload?: TrackingEventPayload): void;
|
|
15
|
+
/**
|
|
16
|
+
* Triggers a manual flush to send all currently buffered events.
|
|
17
|
+
* Useful for serverless functions (like AWS Lambda) before finishing execution.
|
|
18
|
+
*/
|
|
19
|
+
flush(): Promise<void>;
|
|
20
|
+
/**
|
|
21
|
+
* Shuts down the tracker, flushing any remaining events and stopping the interval timers.
|
|
22
|
+
*/
|
|
23
|
+
shutdown(): Promise<void>;
|
|
24
|
+
/**
|
|
25
|
+
* Set up hooks to flush the queue when the Node process is exiting.
|
|
26
|
+
*/
|
|
27
|
+
private setupExitHandlers;
|
|
28
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.GrowlyticsTracker = void 0;
|
|
37
|
+
const os = __importStar(require("os"));
|
|
38
|
+
const http_1 = require("./http");
|
|
39
|
+
const queue_1 = require("./queue");
|
|
40
|
+
const utils_1 = require("./utils");
|
|
41
|
+
const SDK_NAME = 'growlytics-node-sdk';
|
|
42
|
+
const SDK_VERSION = '1.0.0';
|
|
43
|
+
class GrowlyticsTracker {
|
|
44
|
+
config;
|
|
45
|
+
client;
|
|
46
|
+
queue;
|
|
47
|
+
constructor(options = {}) {
|
|
48
|
+
// Standardize configurations with robust default values
|
|
49
|
+
this.config = {
|
|
50
|
+
clientId: options.clientId || 0,
|
|
51
|
+
workspaceId: options.workspaceId || 0,
|
|
52
|
+
applicationId: options.applicationId || 0,
|
|
53
|
+
apiKey: options.apiKey || '',
|
|
54
|
+
baseUrl: options.baseUrl || 'https://api.growlytics.co',
|
|
55
|
+
path: options.path || '/events',
|
|
56
|
+
batchSize: options.batchSize ?? 20,
|
|
57
|
+
flushIntervalMs: options.flushIntervalMs ?? 2000,
|
|
58
|
+
maxQueueSize: options.maxQueueSize ?? 1000,
|
|
59
|
+
debug: options.debug ?? false,
|
|
60
|
+
maxRetries: options.maxRetries ?? 3,
|
|
61
|
+
retryInitialDelayMs: options.retryInitialDelayMs ?? 1000,
|
|
62
|
+
retryMaxDelayMs: options.retryMaxDelayMs ?? 30000,
|
|
63
|
+
onError: options.onError || (() => { }),
|
|
64
|
+
disableAutoContext: options.disableAutoContext ?? false,
|
|
65
|
+
};
|
|
66
|
+
// Initialize the HTTP Client
|
|
67
|
+
this.client = new http_1.HttpClient({
|
|
68
|
+
baseUrl: this.config.baseUrl,
|
|
69
|
+
path: this.config.path,
|
|
70
|
+
apiKey: this.config.apiKey,
|
|
71
|
+
maxRetries: this.config.maxRetries,
|
|
72
|
+
retryInitialDelayMs: this.config.retryInitialDelayMs,
|
|
73
|
+
retryMaxDelayMs: this.config.retryMaxDelayMs,
|
|
74
|
+
debug: this.config.debug,
|
|
75
|
+
});
|
|
76
|
+
// Initialize the Event Queue
|
|
77
|
+
this.queue = new queue_1.EventQueue(this.client, {
|
|
78
|
+
batchSize: this.config.batchSize,
|
|
79
|
+
flushIntervalMs: this.config.flushIntervalMs,
|
|
80
|
+
maxQueueSize: this.config.maxQueueSize,
|
|
81
|
+
debug: this.config.debug,
|
|
82
|
+
onError: this.config.onError,
|
|
83
|
+
});
|
|
84
|
+
this.log('Tracker initialized successfully.');
|
|
85
|
+
this.setupExitHandlers();
|
|
86
|
+
}
|
|
87
|
+
log(message, ...args) {
|
|
88
|
+
if (this.config.debug) {
|
|
89
|
+
console.log(`[Growlytics-SDK] ${message}`, ...args);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Tracks an event by compiling client metadata and placing it in the buffer queue.
|
|
94
|
+
*
|
|
95
|
+
* @param eventType Name of the event, e.g. 'product_view' or 'add_to_cart'
|
|
96
|
+
* @param payload Custom event details and parameters
|
|
97
|
+
*/
|
|
98
|
+
track(eventType, payload = {}) {
|
|
99
|
+
if (!eventType) {
|
|
100
|
+
throw new Error("Event tracking requires a valid 'event_type' / event name.");
|
|
101
|
+
}
|
|
102
|
+
this.log(`Track called for event: ${eventType}`);
|
|
103
|
+
// Auto-detect server device metadata if enabled
|
|
104
|
+
let autoDevice = {};
|
|
105
|
+
if (!this.config.disableAutoContext) {
|
|
106
|
+
try {
|
|
107
|
+
autoDevice = {
|
|
108
|
+
device_type: 'server',
|
|
109
|
+
os: os.platform(),
|
|
110
|
+
os_version: os.release(),
|
|
111
|
+
browser: 'node',
|
|
112
|
+
browser_version: process.version,
|
|
113
|
+
language: process.env.LANG || 'en-US',
|
|
114
|
+
user_agent: `${SDK_NAME}/${SDK_VERSION} (node ${process.version}; ${os.platform()} ${os.release()})`,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
catch (err) {
|
|
118
|
+
this.log('Failed to auto-gather server context details:', err);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
// Prepare SDK library details
|
|
122
|
+
const libraryMetadata = {
|
|
123
|
+
name: SDK_NAME,
|
|
124
|
+
version: SDK_VERSION,
|
|
125
|
+
};
|
|
126
|
+
// Construct the base tracking event with required system keys and auto-generated defaults
|
|
127
|
+
const defaultFields = {
|
|
128
|
+
event_type: eventType,
|
|
129
|
+
client_id: this.config.clientId,
|
|
130
|
+
workspace_id: this.config.workspaceId,
|
|
131
|
+
application_id: this.config.applicationId,
|
|
132
|
+
timestamp: new Date().toISOString(),
|
|
133
|
+
anonymous_id: (0, utils_1.generateUUID)(),
|
|
134
|
+
request_id: (0, utils_1.generateUUID)(),
|
|
135
|
+
library: libraryMetadata,
|
|
136
|
+
};
|
|
137
|
+
// Deep merge payload into defaults. User provided fields will take precedence.
|
|
138
|
+
const compiledEvent = (0, utils_1.mergeDeep)(defaultFields, payload);
|
|
139
|
+
// Explicitly override sub-objects if they need merged device context
|
|
140
|
+
if (!this.config.disableAutoContext) {
|
|
141
|
+
compiledEvent.device = (0, utils_1.mergeDeep)(autoDevice, payload.device || {});
|
|
142
|
+
}
|
|
143
|
+
// Add back the required event_type just in case payload modified it
|
|
144
|
+
compiledEvent.event_type = eventType;
|
|
145
|
+
// Enqueue the finalized event
|
|
146
|
+
this.queue.add(compiledEvent);
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Triggers a manual flush to send all currently buffered events.
|
|
150
|
+
* Useful for serverless functions (like AWS Lambda) before finishing execution.
|
|
151
|
+
*/
|
|
152
|
+
async flush() {
|
|
153
|
+
this.log('Manual flush triggered.');
|
|
154
|
+
await this.queue.flush();
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Shuts down the tracker, flushing any remaining events and stopping the interval timers.
|
|
158
|
+
*/
|
|
159
|
+
async shutdown() {
|
|
160
|
+
this.log('Shutdown initiated.');
|
|
161
|
+
await this.flush();
|
|
162
|
+
this.queue.stop();
|
|
163
|
+
this.log('Tracker stopped.');
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Set up hooks to flush the queue when the Node process is exiting.
|
|
167
|
+
*/
|
|
168
|
+
setupExitHandlers() {
|
|
169
|
+
process.once('beforeExit', () => {
|
|
170
|
+
this.log('Process beforeExit hook triggered. Flushing queue...');
|
|
171
|
+
this.flush().catch((err) => {
|
|
172
|
+
this.log('Failed to flush queue on beforeExit:', err);
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
exports.GrowlyticsTracker = GrowlyticsTracker;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export interface UserIdentifier {
|
|
2
|
+
customer_id?: string;
|
|
3
|
+
external_id?: string;
|
|
4
|
+
email?: string;
|
|
5
|
+
phone?: string;
|
|
6
|
+
}
|
|
7
|
+
export interface InitOptions {
|
|
8
|
+
client_id: number;
|
|
9
|
+
workspace_id: number;
|
|
10
|
+
app_id: number;
|
|
11
|
+
debug?: boolean;
|
|
12
|
+
}
|
|
13
|
+
export interface TrackPayload {
|
|
14
|
+
user_identifier?: UserIdentifier;
|
|
15
|
+
custom?: Record<string, any>;
|
|
16
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generate a cryptographically secure random UUID (v4)
|
|
3
|
+
* Uses native crypto.randomUUID if available (Node 14.17.0+),
|
|
4
|
+
* falls back to a math-based generator for older Node environments.
|
|
5
|
+
*/
|
|
6
|
+
export declare function generateUUID(): string;
|
|
7
|
+
/**
|
|
8
|
+
* Check if a value is a plain object
|
|
9
|
+
*/
|
|
10
|
+
export declare function isObject(item: any): item is Record<string, any>;
|
|
11
|
+
/**
|
|
12
|
+
* Deep merges two objects. User-provided values (source) will overwrite target values,
|
|
13
|
+
* but nested structures will be recursively merged rather than replaced entirely.
|
|
14
|
+
*/
|
|
15
|
+
export declare function mergeDeep<T extends Record<string, any>>(target: T, source: Record<string, any>): T;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.generateUUID = generateUUID;
|
|
37
|
+
exports.isObject = isObject;
|
|
38
|
+
exports.mergeDeep = mergeDeep;
|
|
39
|
+
const crypto = __importStar(require("crypto"));
|
|
40
|
+
/**
|
|
41
|
+
* Generate a cryptographically secure random UUID (v4)
|
|
42
|
+
* Uses native crypto.randomUUID if available (Node 14.17.0+),
|
|
43
|
+
* falls back to a math-based generator for older Node environments.
|
|
44
|
+
*/
|
|
45
|
+
function generateUUID() {
|
|
46
|
+
try {
|
|
47
|
+
if (typeof crypto.randomUUID === 'function') {
|
|
48
|
+
return crypto.randomUUID();
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
catch (e) {
|
|
52
|
+
// Ignore error and fall back
|
|
53
|
+
}
|
|
54
|
+
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
|
|
55
|
+
const r = (Math.random() * 16) | 0;
|
|
56
|
+
const v = c === 'x' ? r : (r & 0x3) | 0x8;
|
|
57
|
+
return v.toString(16);
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Check if a value is a plain object
|
|
62
|
+
*/
|
|
63
|
+
function isObject(item) {
|
|
64
|
+
return item && typeof item === 'object' && !Array.isArray(item);
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Deep merges two objects. User-provided values (source) will overwrite target values,
|
|
68
|
+
* but nested structures will be recursively merged rather than replaced entirely.
|
|
69
|
+
*/
|
|
70
|
+
function mergeDeep(target, source) {
|
|
71
|
+
const output = { ...target };
|
|
72
|
+
if (isObject(target) && isObject(source)) {
|
|
73
|
+
Object.keys(source).forEach((key) => {
|
|
74
|
+
if (isObject(source[key])) {
|
|
75
|
+
if (!(key in target)) {
|
|
76
|
+
Object.assign(output, { [key]: source[key] });
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
output[key] = mergeDeep(target[key], source[key]);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
else if (source[key] !== undefined) {
|
|
83
|
+
Object.assign(output, { [key]: source[key] });
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
return output;
|
|
88
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.registerSections = registerSections;
|
|
4
|
+
exports.buildSections = buildSections;
|
|
5
|
+
const sections = new Map();
|
|
6
|
+
function registerSections(configs) {
|
|
7
|
+
const observer = new IntersectionObserver((entries) => {
|
|
8
|
+
entries.forEach((entry) => {
|
|
9
|
+
const element = entry.target;
|
|
10
|
+
const sectionId = element.dataset.growlyticId;
|
|
11
|
+
let section = sections.get(sectionId);
|
|
12
|
+
if (!section) {
|
|
13
|
+
section = { id: sectionId, visible_time_ms: 0 };
|
|
14
|
+
sections.set(sectionId, section);
|
|
15
|
+
}
|
|
16
|
+
if (entry.isIntersecting) {
|
|
17
|
+
section.start_time = Date.now();
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
if (section.start_time) {
|
|
21
|
+
section.visible_time_ms += Date.now() - section.start_time;
|
|
22
|
+
section.start_time = undefined;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
}, {
|
|
27
|
+
threshold: 0.5
|
|
28
|
+
});
|
|
29
|
+
configs.forEach((config) => {
|
|
30
|
+
const element = document.querySelector(config.selector);
|
|
31
|
+
if (!element) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
element.dataset.growlyticId = config.id;
|
|
35
|
+
observer.observe(element);
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
function buildSections() {
|
|
39
|
+
return Array.from(sections.values()).map((section) => {
|
|
40
|
+
let totalTime = section.visible_time_ms;
|
|
41
|
+
if (section.start_time) {
|
|
42
|
+
totalTime += Date.now() - section.start_time;
|
|
43
|
+
}
|
|
44
|
+
return { section_id: section.id, visible_time_ms: totalTime };
|
|
45
|
+
});
|
|
46
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { UserIdentifier } from "./types";
|
|
2
|
+
import { Engagement } from "./helper_types";
|
|
3
|
+
export interface FinalEventPayload {
|
|
4
|
+
application_id: number;
|
|
5
|
+
event_type: string;
|
|
6
|
+
timestamp: string;
|
|
7
|
+
anonymous_id: string;
|
|
8
|
+
session_id: string;
|
|
9
|
+
request_id: string;
|
|
10
|
+
user_identifier?: UserIdentifier;
|
|
11
|
+
engagement?: Engagement;
|
|
12
|
+
custom?: Record<string, any>;
|
|
13
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { InitOptions, TrackPayload } from "./types";
|
|
2
|
+
declare class GrowlyticSDK {
|
|
3
|
+
private clientId;
|
|
4
|
+
private workspaceId;
|
|
5
|
+
private appId;
|
|
6
|
+
private sessionId;
|
|
7
|
+
private anonymousId;
|
|
8
|
+
init(options: InitOptions): void;
|
|
9
|
+
registerSections(sections: {
|
|
10
|
+
id: string;
|
|
11
|
+
selector: string;
|
|
12
|
+
}[]): void;
|
|
13
|
+
track(eventType: string, payload: TrackPayload): void;
|
|
14
|
+
}
|
|
15
|
+
export declare const Growlytic: GrowlyticSDK;
|
|
16
|
+
export {};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Growlytic = void 0;
|
|
4
|
+
const engagement_1 = require("./engagement");
|
|
5
|
+
class GrowlyticSDK {
|
|
6
|
+
clientId;
|
|
7
|
+
workspaceId;
|
|
8
|
+
appId;
|
|
9
|
+
sessionId;
|
|
10
|
+
anonymousId;
|
|
11
|
+
init(options) {
|
|
12
|
+
this.clientId = options.client_id;
|
|
13
|
+
this.workspaceId = options.workspace_id;
|
|
14
|
+
this.appId = options.app_id;
|
|
15
|
+
const existingAnonymousId = localStorage.getItem("growlytic_anonymous_id");
|
|
16
|
+
const existingSessionId = sessionStorage.getItem("growlytic_session_id");
|
|
17
|
+
if (existingAnonymousId) {
|
|
18
|
+
this.anonymousId = existingAnonymousId;
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
this.anonymousId = crypto.randomUUID();
|
|
22
|
+
localStorage.setItem("growlytic_anonymous_id", this.anonymousId);
|
|
23
|
+
}
|
|
24
|
+
if (existingSessionId) {
|
|
25
|
+
this.sessionId = existingSessionId;
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
this.sessionId = crypto.randomUUID();
|
|
29
|
+
sessionStorage.setItem("growlytic_session_id", this.sessionId);
|
|
30
|
+
}
|
|
31
|
+
console.log("[Nexora] Initialized", options);
|
|
32
|
+
}
|
|
33
|
+
registerSections(sections) {
|
|
34
|
+
(0, engagement_1.registerSections)(sections);
|
|
35
|
+
}
|
|
36
|
+
track(eventType, payload) {
|
|
37
|
+
const event = {
|
|
38
|
+
client_id: this.clientId,
|
|
39
|
+
workspace_id: this.workspaceId,
|
|
40
|
+
app_id: this.appId,
|
|
41
|
+
event_type: eventType,
|
|
42
|
+
timestamp: new Date().toISOString(),
|
|
43
|
+
anonymous_id: this.anonymousId,
|
|
44
|
+
session_id: this.sessionId,
|
|
45
|
+
request_id: crypto.randomUUID(),
|
|
46
|
+
user_identifier: payload.user_identifier,
|
|
47
|
+
engagement: {
|
|
48
|
+
sections: (0, engagement_1.buildSections)()
|
|
49
|
+
},
|
|
50
|
+
custom: payload.custom
|
|
51
|
+
};
|
|
52
|
+
console.log(event);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
exports.Growlytic = new GrowlyticSDK();
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export interface SectionState {
|
|
2
|
+
id: string;
|
|
3
|
+
visible_time_ms: number;
|
|
4
|
+
start_time?: number;
|
|
5
|
+
}
|
|
6
|
+
export interface SectionConfig {
|
|
7
|
+
id: string;
|
|
8
|
+
selector: string;
|
|
9
|
+
}
|
|
10
|
+
export interface SectionEngagement {
|
|
11
|
+
section_id: string;
|
|
12
|
+
visible_time_ms: number;
|
|
13
|
+
}
|
|
14
|
+
export interface Engagement {
|
|
15
|
+
sections: SectionEngagement[];
|
|
16
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { TrackingEvent } from './types';
|
|
2
|
+
export interface HttpClientOptions {
|
|
3
|
+
baseUrl: string;
|
|
4
|
+
path: string;
|
|
5
|
+
apiKey?: string;
|
|
6
|
+
maxRetries: number;
|
|
7
|
+
retryInitialDelayMs: number;
|
|
8
|
+
retryMaxDelayMs: number;
|
|
9
|
+
debug?: boolean;
|
|
10
|
+
}
|
|
11
|
+
export declare class HttpClient {
|
|
12
|
+
private options;
|
|
13
|
+
private url;
|
|
14
|
+
private isHttps;
|
|
15
|
+
constructor(options: HttpClientOptions);
|
|
16
|
+
private log;
|
|
17
|
+
private logError;
|
|
18
|
+
/**
|
|
19
|
+
* Sends a batch of tracking events with automatic retries and exponential backoff.
|
|
20
|
+
*/
|
|
21
|
+
sendEvents(events: TrackingEvent[]): Promise<void>;
|
|
22
|
+
/**
|
|
23
|
+
* Determine if the error is retryable (network drops, 5xx server errors, or 429 rate limits)
|
|
24
|
+
*/
|
|
25
|
+
private isRetryableError;
|
|
26
|
+
/**
|
|
27
|
+
* Calculate exponential backoff delay with full jitter
|
|
28
|
+
*/
|
|
29
|
+
private calculateBackoff;
|
|
30
|
+
private sleep;
|
|
31
|
+
/**
|
|
32
|
+
* Performs the HTTP/HTTPS POST request.
|
|
33
|
+
*/
|
|
34
|
+
private post;
|
|
35
|
+
}
|