@datalyr/api 1.2.0 → 1.2.2

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/README.md CHANGED
@@ -1,195 +1,346 @@
1
1
  # @datalyr/api
2
2
 
3
- Official API SDK for Datalyr server-side tracking with identity resolution support.
3
+ Server-side analytics and attribution SDK for Node.js. Track events, identify users, and preserve attribution data from your backend.
4
4
 
5
5
  ## Installation
6
6
 
7
7
  ```bash
8
8
  npm install @datalyr/api
9
- # or
10
- yarn add @datalyr/api
11
- # or
12
- pnpm add @datalyr/api
13
9
  ```
14
10
 
15
11
  ## Quick Start
16
12
 
17
13
  ```javascript
18
- const { Datalyr } = require('@datalyr/api');
19
- // or
20
14
  import { Datalyr } from '@datalyr/api';
21
15
 
22
- // Initialize with your API key
23
- const datalyr = new Datalyr('your_api_key_here');
16
+ // String shorthand
17
+ const datalyr = new Datalyr('dk_your_api_key');
18
+
19
+ // Or with config object
20
+ const datalyr = new Datalyr({
21
+ apiKey: 'dk_your_api_key',
22
+ debug: true,
23
+ });
24
24
 
25
25
  // Track an event
26
+ await datalyr.track('user_123', 'signup_completed', { plan: 'pro' });
27
+
28
+ // Identify a user
29
+ await datalyr.identify('user_123', { email: 'user@example.com' });
30
+
31
+ // Flush and shut down
32
+ await datalyr.close();
33
+ ```
34
+
35
+ ## Configuration
36
+
37
+ The constructor accepts a `DatalyrConfig` object or an API key string.
38
+
39
+ ```javascript
40
+ // Config object
41
+ const datalyr = new Datalyr({
42
+ apiKey: 'dk_...', // Required. Must start with "dk_".
43
+ host: 'https://ingest.datalyr.com/track', // API endpoint (default: 'https://ingest.datalyr.com/track')
44
+ flushAt: 20, // Flush when queue reaches this size (default: 20, range: 1-100)
45
+ flushInterval: 10000, // Flush timer interval in ms (default: 10000)
46
+ debug: false, // Log events and errors to console (default: false)
47
+ timeout: 10000, // HTTP request timeout in ms (default: 10000, range: 1000-60000)
48
+ retryLimit: 3, // Max retries for failed requests (default: 3)
49
+ maxQueueSize: 1000, // Max queued events before dropping oldest (default: 1000, range: 100-10000)
50
+ });
51
+
52
+ // String shorthand — uses all defaults
53
+ const datalyr = new Datalyr('dk_your_api_key');
54
+ ```
55
+
56
+ ## Methods
57
+
58
+ ### track()
59
+
60
+ Two call signatures:
61
+
62
+ ```javascript
63
+ // Object form (TrackOptions)
64
+ await datalyr.track({
65
+ event: 'Purchase Completed', // Required
66
+ userId: 'user_123', // Optional
67
+ anonymousId: 'anon_from_browser', // Optional — override the auto-generated anonymous ID
68
+ properties: { // Optional
69
+ amount: 99.99,
70
+ currency: 'USD',
71
+ },
72
+ });
73
+
74
+ // Legacy form
26
75
  await datalyr.track('user_123', 'Purchase Completed', {
27
76
  amount: 99.99,
28
77
  currency: 'USD',
29
- products: ['item_1', 'item_2']
30
78
  });
31
79
 
32
- // Identify a user
80
+ // Pass null as userId for anonymous events
81
+ await datalyr.track(null, 'page_loaded', { url: '/pricing' });
82
+ ```
83
+
84
+ ### identify()
85
+
86
+ ```javascript
87
+ await datalyr.identify(userId: string, traits?: any);
88
+ ```
89
+
90
+ Links a user ID to traits. Internally sends a `$identify` event.
91
+
92
+ ```javascript
33
93
  await datalyr.identify('user_123', {
34
94
  email: 'user@example.com',
35
- name: 'John Doe',
36
- plan: 'premium'
95
+ name: 'Jane Doe',
96
+ plan: 'premium',
37
97
  });
98
+ ```
38
99
 
39
- // Track a pageview
40
- await datalyr.page('user_123', 'Homepage', {
41
- url: 'https://example.com',
42
- referrer: 'https://google.com'
100
+ ### page()
101
+
102
+ ```javascript
103
+ await datalyr.page(userId: string, name?: string, properties?: any);
104
+ ```
105
+
106
+ Track a page view. Internally sends a `$pageview` event.
107
+
108
+ ```javascript
109
+ await datalyr.page('user_123', 'Pricing', {
110
+ url: 'https://example.com/pricing',
111
+ referrer: 'https://google.com',
43
112
  });
113
+ ```
44
114
 
45
- // Group a user
115
+ ### group()
116
+
117
+ ```javascript
118
+ await datalyr.group(userId: string, groupId: string, traits?: any);
119
+ ```
120
+
121
+ Associate a user with a group (company, team, etc.). Internally sends a `$group` event.
122
+
123
+ ```javascript
46
124
  await datalyr.group('user_123', 'company_456', {
47
125
  name: 'Acme Corp',
48
- industry: 'Technology'
126
+ industry: 'Technology',
127
+ employees: 50,
49
128
  });
129
+ ```
50
130
 
51
- // Clean up when done
52
- await datalyr.close();
131
+ ### flush()
132
+
133
+ Send all queued events immediately.
134
+
135
+ ```javascript
136
+ await datalyr.flush();
137
+ ```
138
+
139
+ ### close()
140
+
141
+ Stops the flush timer, then attempts a final flush with a **5-second timeout**. Any events still queued after the timeout are dropped. New events tracked after `close()` is called are silently ignored.
142
+
143
+ ```javascript
144
+ process.on('SIGTERM', async () => {
145
+ await datalyr.close();
146
+ process.exit(0);
147
+ });
148
+ ```
149
+
150
+ ### getAnonymousId()
151
+
152
+ Returns the SDK instance's persistent anonymous ID. The ID is generated lazily on first use and has the format `anon_<random><timestamp>` (e.g., `anon_k7x2m9f1lxyzabc`).
153
+
154
+ ```javascript
155
+ const anonId = datalyr.getAnonymousId();
53
156
  ```
54
157
 
55
- ## Identity Resolution (New in v1.1.0)
158
+ ## Event Payload
56
159
 
57
- The SDK now supports anonymous IDs for complete user journey tracking:
160
+ Every event sent to the API has this structure:
161
+
162
+ ```javascript
163
+ {
164
+ event: 'Purchase Completed',
165
+ userId: 'user_123', // undefined if not provided
166
+ anonymousId: 'anon_k7x2m9f...', // Always present
167
+ properties: {
168
+ amount: 99.99,
169
+ anonymous_id: 'anon_k7x2m9f...', // Automatically added
170
+ },
171
+ context: {
172
+ library: '@datalyr/api',
173
+ version: '1.2.1',
174
+ source: 'api',
175
+ },
176
+ timestamp: '2025-01-15T10:30:00.000Z',
177
+ }
178
+ ```
179
+
180
+ Notes:
181
+ - `anonymous_id` is automatically added to `properties` on every event for attribution.
182
+ - The `context` object identifies the SDK and version.
183
+ - `timestamp` is set to the ISO 8601 time when the event was created.
184
+
185
+ ## Batching and Retry Behavior
186
+
187
+ Events are queued locally and sent in batches, not one at a time.
188
+
189
+ - **Auto-flush triggers:** when the queue reaches `flushAt` events, or every `flushInterval` ms.
190
+ - **Batch size:** events are sent in parallel batches of 10 within a single flush.
191
+ - **Queue overflow:** when the queue reaches `maxQueueSize`, the oldest event is dropped to make room.
192
+ - **Retry:** 5xx (server) errors are retried up to `retryLimit` times with exponential backoff (1s, 2s, 4s, ... capped at 10s). 4xx (client) errors are permanent failures and are not retried.
193
+ - **Failed events:** events that fail after all retries are re-queued at the front.
194
+
195
+ ## Attribution Preservation
196
+
197
+ Pass the anonymous ID from your browser or mobile SDK to link server-side events to a client-side session:
58
198
 
59
199
  ```javascript
60
- // Option 1: Pass anonymous_id from browser/mobile for attribution preservation
61
200
  await datalyr.track({
62
201
  event: 'Purchase Completed',
63
202
  userId: 'user_123',
64
- anonymousId: req.body.anonymous_id, // From browser/mobile SDK
203
+ anonymousId: req.body.anonymous_id, // From browser SDK
65
204
  properties: {
66
205
  amount: 99.99,
67
- currency: 'USD'
68
- }
206
+ },
69
207
  });
208
+ ```
70
209
 
71
- // Option 2: Use legacy signature (SDK generates anonymous_id)
72
- await datalyr.track('user_123', 'Purchase Completed', {
73
- amount: 99.99
74
- });
210
+ This preserves UTM parameters, click IDs (gclid, fbclid, ttclid), referrer, landing page, and the full customer journey.
75
211
 
76
- // Get the SDK's anonymous ID (useful for server-only tracking)
77
- const anonymousId = datalyr.getAnonymousId();
78
- ```
212
+ ## Framework Examples
79
213
 
80
- ### Express.js Example with Browser Attribution
214
+ ### Express.js Middleware
81
215
 
82
216
  ```javascript
217
+ import express from 'express';
218
+ import { Datalyr } from '@datalyr/api';
219
+
220
+ const app = express();
221
+ const datalyr = new Datalyr('dk_your_api_key');
222
+
83
223
  app.post('/api/purchase', async (req, res) => {
84
- const { items, anonymous_id } = req.body; // anonymous_id from browser
85
-
86
- // Track with anonymous_id to preserve attribution (fbclid, gclid, etc.)
224
+ const { items, anonymous_id } = req.body;
225
+
87
226
  await datalyr.track({
88
227
  event: 'Purchase Completed',
89
228
  userId: req.user?.id,
90
- anonymousId: anonymous_id, // Links to browser events!
229
+ anonymousId: anonymous_id,
91
230
  properties: {
92
231
  total: calculateTotal(items),
93
- items: items.length
94
- }
232
+ item_count: items.length,
233
+ },
95
234
  });
96
-
235
+
97
236
  res.json({ success: true });
98
237
  });
99
- ```
100
-
101
- ### Key Benefits:
102
- - **Attribution Preservation**: Never lose fbclid, gclid, ttclid, or lyr tracking
103
- - **Complete Journey**: Track users from web → server → mobile
104
- - **Flexible API**: Support both legacy and new tracking methods
105
238
 
106
- ## Configuration
107
-
108
- ```javascript
109
- const datalyr = new Datalyr({
110
- apiKey: 'your_api_key_here',
111
- host: 'https://api.datalyr.com', // Optional: custom host
112
- flushAt: 20, // Optional: batch size (default: 20)
113
- flushInterval: 10000, // Optional: batch interval in ms (default: 10000)
114
- debug: true, // Optional: enable debug logging (default: false)
115
- timeout: 10000, // Optional: request timeout in ms (default: 10000)
116
- retryLimit: 3, // Optional: max retries (default: 3)
117
- maxQueueSize: 1000 // Optional: max events in queue (default: 1000)
239
+ process.on('SIGTERM', async () => {
240
+ await datalyr.close();
241
+ process.exit(0);
118
242
  });
119
243
  ```
120
244
 
121
- ## Stripe Webhook Example
245
+ ### Stripe Webhooks
122
246
 
123
247
  ```javascript
124
- const { Datalyr } = require('@datalyr/api');
125
- const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
248
+ import { Datalyr } from '@datalyr/api';
249
+ import Stripe from 'stripe';
126
250
 
127
- const datalyr = new Datalyr(process.env.DATALYR_API_KEY);
251
+ const datalyr = new Datalyr('dk_your_api_key');
252
+ const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
128
253
 
129
254
  app.post('/webhooks/stripe', async (req, res) => {
130
255
  const sig = req.headers['stripe-signature'];
131
256
  const event = stripe.webhooks.constructEvent(req.body, sig, endpointSecret);
132
-
257
+
133
258
  switch (event.type) {
134
- case 'checkout.session.completed':
135
- await datalyr.track(
136
- event.data.object.client_reference_id,
137
- 'Purchase Completed',
138
- {
139
- amount: event.data.object.amount_total / 100,
140
- currency: event.data.object.currency,
141
- stripeSessionId: event.data.object.id
142
- }
143
- );
259
+ case 'checkout.session.completed': {
260
+ const session = event.data.object;
261
+ await datalyr.track(session.client_reference_id, 'Purchase Completed', {
262
+ amount: session.amount_total / 100,
263
+ currency: session.currency,
264
+ stripe_session_id: session.id,
265
+ });
144
266
  break;
145
-
146
- case 'customer.subscription.created':
147
- await datalyr.track(
148
- event.data.object.metadata.userId,
149
- 'Subscription Started',
150
- {
151
- plan: event.data.object.items.data[0].price.nickname,
152
- mrr: event.data.object.items.data[0].price.unit_amount / 100,
153
- interval: event.data.object.items.data[0].price.recurring.interval
154
- }
155
- );
267
+ }
268
+
269
+ case 'customer.subscription.created': {
270
+ const subscription = event.data.object;
271
+ await datalyr.track(subscription.metadata.userId, 'Subscription Started', {
272
+ plan: subscription.items.data[0].price.nickname,
273
+ mrr: subscription.items.data[0].price.unit_amount / 100,
274
+ interval: subscription.items.data[0].price.recurring.interval,
275
+ });
156
276
  break;
277
+ }
157
278
  }
158
-
279
+
159
280
  res.json({ received: true });
160
281
  });
161
282
  ```
162
283
 
163
- ## API Reference
284
+ ## TypeScript
164
285
 
165
- ### `new Datalyr(config)`
286
+ Full type definitions are included. Exported types:
166
287
 
167
- Creates a new Datalyr instance.
288
+ ```typescript
289
+ import { Datalyr, DatalyrConfig, TrackOptions, TrackEvent } from '@datalyr/api';
168
290
 
169
- ### `track(userId, event, properties?)`
291
+ const config: DatalyrConfig = {
292
+ apiKey: 'dk_your_api_key',
293
+ debug: true,
294
+ };
170
295
 
171
- Track a custom event.
296
+ const datalyr = new Datalyr(config);
172
297
 
173
- ### `identify(userId, traits?)`
298
+ const options: TrackOptions = {
299
+ event: 'Purchase Completed',
300
+ userId: 'user_123',
301
+ anonymousId: 'anon_from_browser',
302
+ properties: {
303
+ amount: 99.99,
304
+ currency: 'USD',
305
+ },
306
+ };
174
307
 
175
- Identify a user with traits.
308
+ await datalyr.track(options);
309
+ ```
310
+
311
+ ## Troubleshooting
176
312
 
177
- ### `page(userId, name?, properties?)`
313
+ **Events not appearing**
178
314
 
179
- Track a pageview.
315
+ 1. Verify your API key starts with `dk_`.
316
+ 2. Enable `debug: true` to see console output.
317
+ 3. Call `await datalyr.flush()` to force-send queued events.
318
+ 4. Check for 4xx errors in debug output -- these indicate a client-side issue (bad API key, malformed payload).
180
319
 
181
- ### `group(userId, groupId, traits?)`
320
+ **Request timeouts**
182
321
 
183
- Associate a user with a group.
322
+ Increase `timeout` and `retryLimit`:
184
323
 
185
- ### `flush()`
324
+ ```javascript
325
+ const datalyr = new Datalyr({
326
+ apiKey: 'dk_your_api_key',
327
+ timeout: 30000,
328
+ retryLimit: 5,
329
+ });
330
+ ```
186
331
 
187
- Manually flush the event queue.
332
+ **Queue full (oldest events dropped)**
188
333
 
189
- ### `close()`
334
+ Increase `maxQueueSize` or flush more aggressively:
190
335
 
191
- Flush remaining events and clean up resources.
336
+ ```javascript
337
+ const datalyr = new Datalyr({
338
+ apiKey: 'dk_your_api_key',
339
+ maxQueueSize: 5000,
340
+ flushAt: 50,
341
+ });
342
+ ```
192
343
 
193
344
  ## License
194
345
 
195
- MIT
346
+ MIT
package/dist/index.js CHANGED
@@ -32,7 +32,7 @@ var Datalyr = class {
32
32
  this.isClosing = false;
33
33
  if (typeof config === "string") {
34
34
  this.apiKey = config;
35
- this.host = "https://api.datalyr.com";
35
+ this.host = "https://ingest.datalyr.com/track";
36
36
  this.debug = false;
37
37
  this.flushAt = 20;
38
38
  this.flushInterval = 1e4;
@@ -41,7 +41,7 @@ var Datalyr = class {
41
41
  this.maxQueueSize = 1e3;
42
42
  } else {
43
43
  this.apiKey = config.apiKey;
44
- this.host = config.host || "https://api.datalyr.com";
44
+ this.host = config.host || "https://ingest.datalyr.com/track";
45
45
  this.debug = config.debug || false;
46
46
  this.flushAt = config.flushAt || 20;
47
47
  this.flushInterval = config.flushInterval || 1e4;
@@ -100,7 +100,7 @@ var Datalyr = class {
100
100
  properties: enrichedProperties,
101
101
  context: {
102
102
  library: "@datalyr/api",
103
- version: "1.1.0",
103
+ version: "1.2.1",
104
104
  source: "api"
105
105
  // Explicitly set source for server-side API
106
106
  },
package/dist/index.mjs CHANGED
@@ -7,7 +7,7 @@ var Datalyr = class {
7
7
  this.isClosing = false;
8
8
  if (typeof config === "string") {
9
9
  this.apiKey = config;
10
- this.host = "https://api.datalyr.com";
10
+ this.host = "https://ingest.datalyr.com/track";
11
11
  this.debug = false;
12
12
  this.flushAt = 20;
13
13
  this.flushInterval = 1e4;
@@ -16,7 +16,7 @@ var Datalyr = class {
16
16
  this.maxQueueSize = 1e3;
17
17
  } else {
18
18
  this.apiKey = config.apiKey;
19
- this.host = config.host || "https://api.datalyr.com";
19
+ this.host = config.host || "https://ingest.datalyr.com/track";
20
20
  this.debug = config.debug || false;
21
21
  this.flushAt = config.flushAt || 20;
22
22
  this.flushInterval = config.flushInterval || 1e4;
@@ -75,7 +75,7 @@ var Datalyr = class {
75
75
  properties: enrichedProperties,
76
76
  context: {
77
77
  library: "@datalyr/api",
78
- version: "1.1.0",
78
+ version: "1.2.1",
79
79
  source: "api"
80
80
  // Explicitly set source for server-side API
81
81
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@datalyr/api",
3
- "version": "1.2.0",
3
+ "version": "1.2.2",
4
4
  "description": "Datalyr API SDK for server-side tracking",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
@@ -43,4 +43,4 @@
43
43
  "engines": {
44
44
  "node": ">=14.0.0"
45
45
  }
46
- }
46
+ }