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
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Growlytics
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
# Growlytics Node.js Tracking SDK
|
|
2
|
+
|
|
3
|
+
An enterprise-grade, high-performance, and resilient Node.js SDK for the Growlytics event tracking platform.
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/growlytics-tracking)
|
|
6
|
+
[](https://opensource.org/licenses/MIT)
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Key Features
|
|
11
|
+
|
|
12
|
+
- **Zero Dependencies**: Keeps your `node_modules` completely clean and free of supply-chain vulnerabilities.
|
|
13
|
+
- **Dual Package Support**: Native ES Module (`esm`) and CommonJS (`cjs`) builds.
|
|
14
|
+
- **TypeScript Support**: Full, detailed TypeScript declarations out of the box for autocomplete on tracking structures.
|
|
15
|
+
- **In-Memory Buffering & Batching**: Groups events together and transmits them in batches to reduce network overhead.
|
|
16
|
+
- **Network Resilience & Retries**: Automatic HTTP requests retries with **exponential backoff and full jitter** for handling intermittent network failures.
|
|
17
|
+
- **Automatic Context Population**: Automatically populates server OS, runtime version, ISO timestamps, and generates random UUIDs for `anonymous_id` and `request_id` if missing.
|
|
18
|
+
- **Graceful Shutdown**: Automatically flushes queued events when your Node process exits.
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## Installation
|
|
23
|
+
|
|
24
|
+
Install via npm:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
npm install growlytics-tracking
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Or via yarn:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
yarn add growlytics-tracking
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Quick Start
|
|
39
|
+
|
|
40
|
+
### 1. Global Singleton Pattern (Recommended)
|
|
41
|
+
|
|
42
|
+
Ideal for single-instance web servers (e.g. Express, Koa, NestJS). Call `initiate` once at app startup, then import and track events anywhere.
|
|
43
|
+
|
|
44
|
+
**CommonJS (JavaScript):**
|
|
45
|
+
```javascript
|
|
46
|
+
const Growlytics = require('growlytics-tracking');
|
|
47
|
+
|
|
48
|
+
// Initialize the tracker once at application startup
|
|
49
|
+
Growlytics.initiate({
|
|
50
|
+
clientId: 1001,
|
|
51
|
+
workspaceId: 2001,
|
|
52
|
+
applicationId: 3001,
|
|
53
|
+
apiKey: 'your-api-key-here',
|
|
54
|
+
debug: false
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// Track events anywhere in your code
|
|
58
|
+
Growlytics.track('product_view', {
|
|
59
|
+
session_id: 'sess_123',
|
|
60
|
+
user_identifier: {
|
|
61
|
+
customer_id: 'cust_123',
|
|
62
|
+
email: 'customer@gmail.com',
|
|
63
|
+
},
|
|
64
|
+
product: {
|
|
65
|
+
product_id: 'SKU123',
|
|
66
|
+
name: 'Bathroom Mat',
|
|
67
|
+
price: 499,
|
|
68
|
+
currency: 'INR'
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
**ES Modules (TypeScript / JavaScript):**
|
|
74
|
+
```typescript
|
|
75
|
+
import Growlytics from 'growlytics-tracking';
|
|
76
|
+
|
|
77
|
+
// Initialize
|
|
78
|
+
Growlytics.initiate({
|
|
79
|
+
clientId: 1001,
|
|
80
|
+
workspaceId: 2001,
|
|
81
|
+
applicationId: 3001,
|
|
82
|
+
apiKey: 'your-api-key-here',
|
|
83
|
+
batchSize: 20,
|
|
84
|
+
flushIntervalMs: 2000
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// Track
|
|
88
|
+
Growlytics.track('add_to_cart', {
|
|
89
|
+
session_id: 'sess_123',
|
|
90
|
+
cart: {
|
|
91
|
+
cart_id: 'cart_123',
|
|
92
|
+
total_items: 1,
|
|
93
|
+
total_amount: 499
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### 2. Multi-Instance Pattern
|
|
99
|
+
|
|
100
|
+
If your application needs to connect to multiple workspaces or clients, instantiate the tracker class directly:
|
|
101
|
+
|
|
102
|
+
```typescript
|
|
103
|
+
import { GrowlyticsTracker } from 'growlytics-tracking';
|
|
104
|
+
|
|
105
|
+
const trackerA = new GrowlyticsTracker({
|
|
106
|
+
clientId: 1001,
|
|
107
|
+
workspaceId: 2001,
|
|
108
|
+
apiKey: 'key-a'
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
const trackerB = new GrowlyticsTracker({
|
|
112
|
+
clientId: 5005,
|
|
113
|
+
workspaceId: 9009,
|
|
114
|
+
apiKey: 'key-b'
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
trackerA.track('custom_event', { data: 'value' });
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
## SDK Configuration Options
|
|
123
|
+
|
|
124
|
+
Initialize the SDK with the following configuration options:
|
|
125
|
+
|
|
126
|
+
| Option | Type | Default | Description |
|
|
127
|
+
|---|---|---|---|
|
|
128
|
+
| `clientId` | `number` | `0` | Fallback Client ID (if not provided per event payload). |
|
|
129
|
+
| `workspaceId` | `number` | `0` | Fallback Workspace ID (if not provided per event payload). |
|
|
130
|
+
| `applicationId` | `number` | `0` | Fallback Application ID (if not provided per event payload). |
|
|
131
|
+
| `apiKey` | `string` | `""` | Authentication write key passed via the `Authorization` header. |
|
|
132
|
+
| `baseUrl` | `string` | `"https://api.growlytics.co"` | Base URL of the Growlytics event ingestion endpoint. |
|
|
133
|
+
| `path` | `string` | `"/events"` | Ingestion path appended to `baseUrl`. |
|
|
134
|
+
| `batchSize` | `number` | `20` | Max number of events sent in a single HTTP batch. |
|
|
135
|
+
| `flushIntervalMs` | `number` | `2000` | Periodically flushes queued events (in milliseconds). |
|
|
136
|
+
| `maxQueueSize` | `number` | `1000` | Max buffer capacity. Discards oldest events if queue overflows. |
|
|
137
|
+
| `maxRetries` | `number` | `3` | Number of times to retry failed connection drops/5xx responses. |
|
|
138
|
+
| `retryInitialDelayMs` | `number` | `1000` | Base delay for the first exponential retry. |
|
|
139
|
+
| `retryMaxDelayMs` | `number` | `30000` | Maximum cap on retry backoff delay. |
|
|
140
|
+
| `debug` | `boolean` | `false` | Enables logs for troubleshooting. |
|
|
141
|
+
| `disableAutoContext` | `boolean` | `false` | Set to true to disable automatic server environment context capture. |
|
|
142
|
+
| `onError` | `function` | `undefined` | Callback invoked when a batch fails permanently after all retries. |
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
## Automatic Context and Value Generations
|
|
147
|
+
|
|
148
|
+
To save developer time and ensure data reliability, the SDK automatically generates and merges fields:
|
|
149
|
+
|
|
150
|
+
- **Timestamps**: If `timestamp` is omitted in the event, the current system ISO time is added.
|
|
151
|
+
- **UUID Generations**:
|
|
152
|
+
- `anonymous_id`: If not provided, a random, cryptographically secure **UUID v4** is generated for the event.
|
|
153
|
+
- `request_id`: A unique, random **UUID v4** is generated for every tracked event to prevent duplicate ingestion.
|
|
154
|
+
- **Device Details**: Unless `disableAutoContext` is set to `true`, the SDK gathers the server environment (Node version, OS platform, and OS release) and populates the `device` object:
|
|
155
|
+
```json
|
|
156
|
+
"device": {
|
|
157
|
+
"device_type": "server",
|
|
158
|
+
"os": "darwin",
|
|
159
|
+
"os_version": "21.6.0",
|
|
160
|
+
"browser": "node",
|
|
161
|
+
"browser_version": "v18.16.0",
|
|
162
|
+
"language": "en-US",
|
|
163
|
+
"user_agent": "growlytics-node-sdk/1.0.0 (node v18.16.0; darwin 21.6.0)"
|
|
164
|
+
}
|
|
165
|
+
```
|
|
166
|
+
*Note: Any nested values provided inside the event `device` parameter will override these defaults.*
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
## Advanced Usage
|
|
171
|
+
|
|
172
|
+
### 1. Serverless Environments (AWS Lambda, Vercel)
|
|
173
|
+
|
|
174
|
+
In serverless execution environments, Node's event loop may be frozen immediately after returning a response. To guarantee that tracked events reach Growlytics before the environment suspends, call `.flush()` synchronously before terminating your function:
|
|
175
|
+
|
|
176
|
+
```javascript
|
|
177
|
+
exports.handler = async (event) => {
|
|
178
|
+
Growlytics.track('lambda_execution', { function_name: 'payment-processor' });
|
|
179
|
+
|
|
180
|
+
// Force flush the tracking queue immediately
|
|
181
|
+
await Growlytics.flush();
|
|
182
|
+
|
|
183
|
+
return { statusCode: 200, body: 'Done' };
|
|
184
|
+
};
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
### 2. Failure Callbacks (`onError`)
|
|
188
|
+
|
|
189
|
+
If you want to log permanent failure payloads to a fallback logger, database, or alerting tool, register the `onError` hook:
|
|
190
|
+
|
|
191
|
+
```javascript
|
|
192
|
+
Growlytics.initiate({
|
|
193
|
+
clientId: 1001,
|
|
194
|
+
onError: (error, failedEvents) => {
|
|
195
|
+
console.error(`Growlytics Delivery Failure: ${error.message}`);
|
|
196
|
+
// Save to backup file or dispatch to alerting systems
|
|
197
|
+
backupLogger.log(failedEvents);
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
---
|
|
203
|
+
|
|
204
|
+
## License
|
|
205
|
+
|
|
206
|
+
MIT License. Copyright (c) Growlytics.
|
|
@@ -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
|
+
}
|
package/dist/cjs/http.js
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
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.HttpClient = void 0;
|
|
37
|
+
const http = __importStar(require("http"));
|
|
38
|
+
const https = __importStar(require("https"));
|
|
39
|
+
const url_1 = require("url");
|
|
40
|
+
class HttpClient {
|
|
41
|
+
options;
|
|
42
|
+
url;
|
|
43
|
+
isHttps;
|
|
44
|
+
constructor(options) {
|
|
45
|
+
this.options = options;
|
|
46
|
+
// Standardize baseline endpoint
|
|
47
|
+
const urlString = options.baseUrl.endsWith('/')
|
|
48
|
+
? options.baseUrl.slice(0, -1) + options.path
|
|
49
|
+
: options.baseUrl + options.path;
|
|
50
|
+
this.url = new url_1.URL(urlString);
|
|
51
|
+
this.isHttps = this.url.protocol === 'https:';
|
|
52
|
+
}
|
|
53
|
+
log(message, ...args) {
|
|
54
|
+
if (this.options.debug) {
|
|
55
|
+
console.log(`[Growlytics-SDK][HTTP] ${message}`, ...args);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
logError(message, ...args) {
|
|
59
|
+
if (this.options.debug) {
|
|
60
|
+
console.error(`[Growlytics-SDK][HTTP][ERROR] ${message}`, ...args);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Sends a batch of tracking events with automatic retries and exponential backoff.
|
|
65
|
+
*/
|
|
66
|
+
async sendEvents(events) {
|
|
67
|
+
let attempt = 0;
|
|
68
|
+
const maxAttempts = this.options.maxRetries + 1;
|
|
69
|
+
while (attempt < maxAttempts) {
|
|
70
|
+
try {
|
|
71
|
+
await this.post(events);
|
|
72
|
+
this.log(`Successfully sent batch of ${events.length} events.`);
|
|
73
|
+
return; // Success!
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
attempt++;
|
|
77
|
+
const isRetryable = this.isRetryableError(error);
|
|
78
|
+
if (attempt >= maxAttempts || !isRetryable) {
|
|
79
|
+
this.logError(`Failed to send batch of ${events.length} events after ${attempt} attempts. Permanent failure.`);
|
|
80
|
+
throw error;
|
|
81
|
+
}
|
|
82
|
+
// Calculate backoff delay with jitter
|
|
83
|
+
const delay = this.calculateBackoff(attempt);
|
|
84
|
+
this.logError(`Attempt ${attempt} failed: ${error.message || error}. Retrying in ${delay}ms...`);
|
|
85
|
+
await this.sleep(delay);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Determine if the error is retryable (network drops, 5xx server errors, or 429 rate limits)
|
|
91
|
+
*/
|
|
92
|
+
isRetryableError(error) {
|
|
93
|
+
// If it's a network-level connection error, it's retryable
|
|
94
|
+
if (error.code) {
|
|
95
|
+
const retryableCodes = ['ECONNRESET', 'EADDRINUSE', 'ETIMEDOUT', 'ECONNREFUSED', 'ENOTFOUND', 'EPIPE'];
|
|
96
|
+
if (retryableCodes.includes(error.code)) {
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
// Check status code if it's an HTTP response error
|
|
101
|
+
if (error.statusCode) {
|
|
102
|
+
const status = error.statusCode;
|
|
103
|
+
// Retry on 429 (Rate Limit) and any 5xx (Server Error)
|
|
104
|
+
return status === 429 || (status >= 500 && status < 600);
|
|
105
|
+
}
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Calculate exponential backoff delay with full jitter
|
|
110
|
+
*/
|
|
111
|
+
calculateBackoff(attempt) {
|
|
112
|
+
const delay = this.options.retryInitialDelayMs * Math.pow(2, attempt - 1);
|
|
113
|
+
const cappedDelay = Math.min(this.options.retryMaxDelayMs, delay);
|
|
114
|
+
// Apply jitter: randomize between 50% and 100% of the calculated delay
|
|
115
|
+
const jitter = 0.5 + Math.random() * 0.5;
|
|
116
|
+
return Math.floor(cappedDelay * jitter);
|
|
117
|
+
}
|
|
118
|
+
sleep(ms) {
|
|
119
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Performs the HTTP/HTTPS POST request.
|
|
123
|
+
*/
|
|
124
|
+
post(events) {
|
|
125
|
+
return new Promise((resolve, reject) => {
|
|
126
|
+
const payloadString = JSON.stringify(events);
|
|
127
|
+
const headers = {
|
|
128
|
+
'Content-Type': 'application/json',
|
|
129
|
+
'Content-Length': Buffer.byteLength(payloadString),
|
|
130
|
+
'User-Agent': 'growlytics-node-sdk/1.0.0',
|
|
131
|
+
};
|
|
132
|
+
if (this.options.apiKey) {
|
|
133
|
+
headers['Authorization'] = `Bearer ${this.options.apiKey}`;
|
|
134
|
+
headers['X-Growlytics-Key'] = this.options.apiKey; // Alternate standard tracking header
|
|
135
|
+
}
|
|
136
|
+
const requestOptions = {
|
|
137
|
+
hostname: this.url.hostname,
|
|
138
|
+
port: this.url.port || (this.isHttps ? 443 : 80),
|
|
139
|
+
path: this.url.pathname + this.url.search,
|
|
140
|
+
method: 'POST',
|
|
141
|
+
headers: headers,
|
|
142
|
+
timeout: 10000, // 10 second timeout
|
|
143
|
+
};
|
|
144
|
+
const requestModule = this.isHttps ? https : http;
|
|
145
|
+
const req = requestModule.request(requestOptions, (res) => {
|
|
146
|
+
let responseBody = '';
|
|
147
|
+
res.on('data', (chunk) => {
|
|
148
|
+
responseBody += chunk;
|
|
149
|
+
});
|
|
150
|
+
res.on('end', () => {
|
|
151
|
+
const status = res.statusCode || 0;
|
|
152
|
+
if (status >= 200 && status < 300) {
|
|
153
|
+
resolve();
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
const error = new Error(`HTTP Error Status ${status}: ${responseBody || res.statusMessage}`);
|
|
157
|
+
error.statusCode = status;
|
|
158
|
+
reject(error);
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
req.on('error', (err) => {
|
|
163
|
+
reject(err);
|
|
164
|
+
});
|
|
165
|
+
req.on('timeout', () => {
|
|
166
|
+
req.destroy();
|
|
167
|
+
const err = new Error('Request timed out');
|
|
168
|
+
err.code = 'ETIMEDOUT';
|
|
169
|
+
reject(err);
|
|
170
|
+
});
|
|
171
|
+
req.write(payloadString);
|
|
172
|
+
req.end();
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
exports.HttpClient = HttpClient;
|
|
@@ -0,0 +1,20 @@
|
|
|
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 __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
exports.Growlytic = void 0;
|
|
18
|
+
__exportStar(require("./types"), exports);
|
|
19
|
+
var growlytics_1 = require("./growlytics");
|
|
20
|
+
Object.defineProperty(exports, "Growlytic", { enumerable: true, get: function () { return growlytics_1.Growlytic; } });
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { TrackingEvent } from './types';
|
|
2
|
+
import { HttpClient } from './http';
|
|
3
|
+
export interface QueueConfig {
|
|
4
|
+
batchSize: number;
|
|
5
|
+
flushIntervalMs: number;
|
|
6
|
+
maxQueueSize: number;
|
|
7
|
+
debug?: boolean;
|
|
8
|
+
onError?: (error: Error, events: TrackingEvent[]) => void;
|
|
9
|
+
}
|
|
10
|
+
export declare class EventQueue {
|
|
11
|
+
private client;
|
|
12
|
+
private config;
|
|
13
|
+
private queue;
|
|
14
|
+
private timer;
|
|
15
|
+
private isFlushing;
|
|
16
|
+
private activeFlushPromise;
|
|
17
|
+
constructor(client: HttpClient, config: QueueConfig);
|
|
18
|
+
private log;
|
|
19
|
+
/**
|
|
20
|
+
* Add a tracking event to the queue. If the queue is full, the oldest event is discarded.
|
|
21
|
+
*/
|
|
22
|
+
add(event: TrackingEvent): void;
|
|
23
|
+
/**
|
|
24
|
+
* Manually trigger a flush of all currently queued events.
|
|
25
|
+
* Returns a promise that resolves when the flush is complete.
|
|
26
|
+
*/
|
|
27
|
+
flush(): Promise<void>;
|
|
28
|
+
/**
|
|
29
|
+
* Performs the flush logic, calling the HttpClient to send batches of events.
|
|
30
|
+
*/
|
|
31
|
+
private performFlush;
|
|
32
|
+
private startTimer;
|
|
33
|
+
/**
|
|
34
|
+
* Resets the background interval timer.
|
|
35
|
+
*/
|
|
36
|
+
private resetTimer;
|
|
37
|
+
/**
|
|
38
|
+
* Stops the background timer.
|
|
39
|
+
*/
|
|
40
|
+
stop(): void;
|
|
41
|
+
/**
|
|
42
|
+
* Get the current count of events in the queue.
|
|
43
|
+
*/
|
|
44
|
+
get length(): number;
|
|
45
|
+
}
|