enrich.sh 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +276 -0
- package/package.json +41 -0
- package/src/index.d.ts +111 -0
- package/src/index.js +256 -0
package/README.md
ADDED
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
# enrich.sh
|
|
2
|
+
|
|
3
|
+
```
|
|
4
|
+
┌─────────────────────────────────────────────────┐
|
|
5
|
+
│ │
|
|
6
|
+
│ ███████╗███╗ ██╗██████╗ ██╗ ██████╗██╗ ██╗ │
|
|
7
|
+
│ ██╔════╝████╗ ██║██╔══██╗██║██╔════╝██║ ██║ │
|
|
8
|
+
│ █████╗ ██╔██╗ ██║██████╔╝██║██║ ███████║ │
|
|
9
|
+
│ ██╔══╝ ██║╚██╗██║██╔══██╗██║██║ ██╔══██║ │
|
|
10
|
+
│ ███████╗██║ ╚████║██║ ██║██║╚██████╗██║ ██║ │
|
|
11
|
+
│ ╚══════╝╚═╝ ╚═══╝╚═╝ ╚═╝╚═╝ ╚═════╝╚═╝ ╚═╝ │
|
|
12
|
+
│ │
|
|
13
|
+
│ Serverless Data Ingestion Pipeline │
|
|
14
|
+
│ Your events → Parquet on R2 → DuckDB │
|
|
15
|
+
│ │
|
|
16
|
+
└─────────────────────────────────────────────────┘
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Official JavaScript SDK for **[Enrich.sh](https://get.enrich.sh)**
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Install
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npm install enrich.sh
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Quick Start
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
┌──────────┐ track() ┌──────────┐ flush ┌──────────┐
|
|
33
|
+
│ Your │ ─────────────► │ SDK │ ──────────► │ Enrich │
|
|
34
|
+
│ App │ (buffered) │ Buffer │ (batched) │ API │
|
|
35
|
+
└──────────┘ └──────────┘ └────┬─────┘
|
|
36
|
+
│
|
|
37
|
+
┌────▼─────┐
|
|
38
|
+
│ R2 │
|
|
39
|
+
│ Parquet │
|
|
40
|
+
└────┬─────┘
|
|
41
|
+
│
|
|
42
|
+
┌────▼─────┐
|
|
43
|
+
│ DuckDB │
|
|
44
|
+
│ Query │
|
|
45
|
+
└──────────┘
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### 1 · Initialize
|
|
49
|
+
|
|
50
|
+
```javascript
|
|
51
|
+
import { Enrich } from 'enrich.sh';
|
|
52
|
+
|
|
53
|
+
const enrich = new Enrich('sk_live_your_api_key');
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### 2 · Track Events
|
|
57
|
+
|
|
58
|
+
Events are buffered locally, then flushed in batches.
|
|
59
|
+
|
|
60
|
+
```javascript
|
|
61
|
+
enrich.track('page_views', {
|
|
62
|
+
url: window.location.href,
|
|
63
|
+
referrer: document.referrer,
|
|
64
|
+
user_id: 'usr_42',
|
|
65
|
+
});
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### 3 · Direct Ingestion
|
|
69
|
+
|
|
70
|
+
Send a batch immediately — useful server-side.
|
|
71
|
+
|
|
72
|
+
```javascript
|
|
73
|
+
await enrich.ingest('purchases', [
|
|
74
|
+
{ item: 'Laptop', price: 999, user_id: 'usr_42' },
|
|
75
|
+
{ item: 'Mouse', price: 25, user_id: 'usr_77' },
|
|
76
|
+
]);
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### 4 · Query with DuckDB
|
|
80
|
+
|
|
81
|
+
Get presigned URLs, pass them straight to DuckDB.
|
|
82
|
+
|
|
83
|
+
```javascript
|
|
84
|
+
const urls = await enrich.query('page_views', { days: 7 });
|
|
85
|
+
|
|
86
|
+
// DuckDB (JS/WASM)
|
|
87
|
+
await conn.query(`SELECT * FROM read_parquet(${JSON.stringify(urls)})`);
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## API Reference
|
|
93
|
+
|
|
94
|
+
```
|
|
95
|
+
┌───────────────────────────────────────────────────────────────┐
|
|
96
|
+
│ METHOD │ DESCRIPTION │
|
|
97
|
+
├───────────────────────┼───────────────────────────────────────┤
|
|
98
|
+
│ new Enrich(key, opt) │ Create client instance │
|
|
99
|
+
│ .track(id, event) │ Buffer event for batched delivery │
|
|
100
|
+
│ .ingest(id, data) │ Send immediately (returns Promise) │
|
|
101
|
+
│ .query(id, params) │ Get signed Parquet URLs │
|
|
102
|
+
│ .queryDetailed(id,p) │ Get URLs + file metadata │
|
|
103
|
+
│ .flush([id]) │ Force-flush one or all buffers │
|
|
104
|
+
│ .beacon([id]) │ Keepalive flush (tab close safe) │
|
|
105
|
+
│ .destroy() │ Flush + cleanup timers │
|
|
106
|
+
└───────────────────────┴───────────────────────────────────────┘
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### `new Enrich(apiKey, options?)`
|
|
110
|
+
|
|
111
|
+
| Option | Default | Description |
|
|
112
|
+
| :-------------- | :------------------- | :-------------------------------- |
|
|
113
|
+
| `baseUrl` | `https://enrich.sh` | API endpoint |
|
|
114
|
+
| `batchSize` | `100` | Events buffered before auto-flush |
|
|
115
|
+
| `flushInterval` | `5000` | Max ms between flushes |
|
|
116
|
+
| `maxRetries` | `2` | Retry attempts on flush failure |
|
|
117
|
+
|
|
118
|
+
### `.track(streamId, event)`
|
|
119
|
+
|
|
120
|
+
Buffers a single event. Auto-flushes at `batchSize` or every `flushInterval` ms.
|
|
121
|
+
|
|
122
|
+
### `.ingest(streamId, data)`
|
|
123
|
+
|
|
124
|
+
Sends one event or an array immediately. Returns the API response.
|
|
125
|
+
|
|
126
|
+
### `.query(streamId, params?)`
|
|
127
|
+
|
|
128
|
+
Returns `string[]` — signed URLs for Parquet files.
|
|
129
|
+
|
|
130
|
+
| Param | Format | Example |
|
|
131
|
+
| :------ | :----------- | :------------- |
|
|
132
|
+
| `date` | `YYYY-MM-DD` | `2026-02-11` |
|
|
133
|
+
| `start` | `YYYY-MM-DD` | `2026-02-01` |
|
|
134
|
+
| `end` | `YYYY-MM-DD` | `2026-02-10` |
|
|
135
|
+
| `days` | number | `7` |
|
|
136
|
+
|
|
137
|
+
### `.queryDetailed(streamId, params?)`
|
|
138
|
+
|
|
139
|
+
Same params as `.query()`. Returns full response:
|
|
140
|
+
|
|
141
|
+
```json
|
|
142
|
+
{
|
|
143
|
+
"urls": ["https://..."],
|
|
144
|
+
"files": [{ "key": "...", "url": "...", "size": 1024, "uploaded_at": "..." }],
|
|
145
|
+
"file_count": 3,
|
|
146
|
+
"expires_at": "2026-02-12T..."
|
|
147
|
+
}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### `.flush(streamId?)`
|
|
151
|
+
|
|
152
|
+
Force-send buffered events. Omit `streamId` to flush all.
|
|
153
|
+
|
|
154
|
+
### `.beacon(streamId?)`
|
|
155
|
+
|
|
156
|
+
Best-effort flush that survives page unload/tab close.
|
|
157
|
+
Uses `fetch({ keepalive: true })` with proper `Authorization` headers.
|
|
158
|
+
Fire-and-forget — does not return a Promise.
|
|
159
|
+
|
|
160
|
+
### `.destroy()`
|
|
161
|
+
|
|
162
|
+
Flush everything and clear timers. Call before process exit.
|
|
163
|
+
|
|
164
|
+
---
|
|
165
|
+
|
|
166
|
+
## Limits & Specs
|
|
167
|
+
|
|
168
|
+
```
|
|
169
|
+
┌───────────────────────────────────────────────────────────────┐
|
|
170
|
+
│ SPEC │ VALUE │
|
|
171
|
+
├───────────────────────┼───────────────────────────────────────┤
|
|
172
|
+
│ Max request size │ 1 MB │
|
|
173
|
+
│ Max events / request │ ~10,000 (depending on event size) │
|
|
174
|
+
│ Signed URL TTL │ 24 hours │
|
|
175
|
+
│ Output format │ Apache Parquet │
|
|
176
|
+
│ Auth │ Bearer token (sk_live_* / sk_test_*)│
|
|
177
|
+
│ Transport │ HTTPS │
|
|
178
|
+
│ Runtime │ Node 18+, Browsers, Edge Workers │
|
|
179
|
+
│ Dependencies │ 0 │
|
|
180
|
+
│ Package size │ < 3 KB │
|
|
181
|
+
└───────────────────────┴───────────────────────────────────────┘
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
### Rate Limits by Plan
|
|
185
|
+
|
|
186
|
+
```
|
|
187
|
+
┌──────────────┬──────────────┬────────────┬────────────────────┐
|
|
188
|
+
│ Plan │ Events/mo │ Streams │ Retention │
|
|
189
|
+
├──────────────┼──────────────┼────────────┼────────────────────┤
|
|
190
|
+
│ Test Key │ 100K │ 3 │ 30 days │
|
|
191
|
+
│ Starter │ 5M │ 3 │ 30 days │
|
|
192
|
+
│ Pro │ 100M │ Unlimited │ 90 days │
|
|
193
|
+
│ Scale │ 500M │ Unlimited │ 1 year │
|
|
194
|
+
│ Enterprise │ Custom │ Unlimited │ Custom │
|
|
195
|
+
└──────────────┴──────────────┴────────────┴────────────────────┘
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
---
|
|
199
|
+
|
|
200
|
+
## Examples
|
|
201
|
+
|
|
202
|
+
### Browser — Clickstream
|
|
203
|
+
|
|
204
|
+
```javascript
|
|
205
|
+
import { Enrich } from 'enrich.sh';
|
|
206
|
+
|
|
207
|
+
const enrich = new Enrich('sk_live_xxx');
|
|
208
|
+
|
|
209
|
+
document.addEventListener('click', (e) => {
|
|
210
|
+
enrich.track('clicks', {
|
|
211
|
+
tag: e.target.tagName,
|
|
212
|
+
id: e.target.id,
|
|
213
|
+
path: location.pathname,
|
|
214
|
+
ts: new Date().toISOString(),
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
// Flush before tab close (beacon survives page unload)
|
|
219
|
+
window.addEventListener('beforeunload', () => enrich.beacon());
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
### Node.js — Server-side
|
|
223
|
+
|
|
224
|
+
```javascript
|
|
225
|
+
import { Enrich } from 'enrich.sh';
|
|
226
|
+
|
|
227
|
+
const enrich = new Enrich('sk_live_xxx');
|
|
228
|
+
|
|
229
|
+
app.post('/checkout', async (req, res) => {
|
|
230
|
+
await enrich.ingest('transactions', {
|
|
231
|
+
order_id: req.body.id,
|
|
232
|
+
amount: req.body.total,
|
|
233
|
+
currency: 'USD',
|
|
234
|
+
});
|
|
235
|
+
res.json({ ok: true });
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
// Cleanup on shutdown
|
|
239
|
+
process.on('SIGTERM', () => enrich.destroy());
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
### DuckDB — Analytics
|
|
243
|
+
|
|
244
|
+
```javascript
|
|
245
|
+
import * as duckdb from '@duckdb/duckdb-wasm';
|
|
246
|
+
import { Enrich } from 'enrich.sh';
|
|
247
|
+
|
|
248
|
+
const enrich = new Enrich('sk_live_xxx');
|
|
249
|
+
const urls = await enrich.query('clicks', { days: 30 });
|
|
250
|
+
|
|
251
|
+
const conn = await db.connect();
|
|
252
|
+
const result = await conn.query(`
|
|
253
|
+
SELECT path, COUNT(*) as hits
|
|
254
|
+
FROM read_parquet(${JSON.stringify(urls)})
|
|
255
|
+
GROUP BY path
|
|
256
|
+
ORDER BY hits DESC
|
|
257
|
+
`);
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
---
|
|
261
|
+
|
|
262
|
+
## Keys
|
|
263
|
+
|
|
264
|
+
```
|
|
265
|
+
sk_test_* → Sandbox (100K events, free tier limits)
|
|
266
|
+
sk_live_* → Production (paid plan limits apply)
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
Test keys work identically to live keys but enforce free-tier caps.
|
|
270
|
+
Get your keys at **[dashboard.enrich.sh](https://dashboard.enrich.sh)**
|
|
271
|
+
|
|
272
|
+
---
|
|
273
|
+
|
|
274
|
+
## License
|
|
275
|
+
|
|
276
|
+
MIT — [Enrich.sh](https://get.enrich.sh)
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "enrich.sh",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Official SDK for Enrich.sh — Serverless Data Ingestion Pipeline. Ingest → Parquet → DuckDB.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"enrich",
|
|
7
|
+
"analytics",
|
|
8
|
+
"ingestion",
|
|
9
|
+
"data-pipeline",
|
|
10
|
+
"parquet",
|
|
11
|
+
"duckdb",
|
|
12
|
+
"clickstream",
|
|
13
|
+
"serverless"
|
|
14
|
+
],
|
|
15
|
+
"homepage": "https://get.enrich.sh",
|
|
16
|
+
"bugs": {
|
|
17
|
+
"url": "https://github.com/enrich-sh/sdk-js/issues"
|
|
18
|
+
},
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "git+https://github.com/enrich-sh/sdk-js.git"
|
|
22
|
+
},
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"author": "Enrich.sh",
|
|
25
|
+
"type": "module",
|
|
26
|
+
"main": "src/index.js",
|
|
27
|
+
"types": "src/index.d.ts",
|
|
28
|
+
"exports": {
|
|
29
|
+
".": "./src/index.js"
|
|
30
|
+
},
|
|
31
|
+
"files": [
|
|
32
|
+
"src",
|
|
33
|
+
"README.md"
|
|
34
|
+
],
|
|
35
|
+
"scripts": {
|
|
36
|
+
"test": "node test.js"
|
|
37
|
+
},
|
|
38
|
+
"engines": {
|
|
39
|
+
"node": ">=18"
|
|
40
|
+
}
|
|
41
|
+
}
|
package/src/index.d.ts
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Enrich.sh SDK
|
|
3
|
+
* Official client for data ingestion and retrieval.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface EnrichOptions {
|
|
7
|
+
/** API base URL. Default: "https://enrich.sh" */
|
|
8
|
+
baseUrl?: string;
|
|
9
|
+
/** Events buffered before auto-flush. Default: 100 */
|
|
10
|
+
batchSize?: number;
|
|
11
|
+
/** Max milliseconds between flushes. Default: 5000 */
|
|
12
|
+
flushInterval?: number;
|
|
13
|
+
/** Retry attempts on flush failure. Default: 2 */
|
|
14
|
+
maxRetries?: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface QueryParams {
|
|
18
|
+
/** Single day — YYYY-MM-DD */
|
|
19
|
+
date?: string;
|
|
20
|
+
/** Range start — YYYY-MM-DD */
|
|
21
|
+
start?: string;
|
|
22
|
+
/** Range end — YYYY-MM-DD */
|
|
23
|
+
end?: string;
|
|
24
|
+
/** Last N days */
|
|
25
|
+
days?: number | string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface FileInfo {
|
|
29
|
+
key: string;
|
|
30
|
+
url: string;
|
|
31
|
+
size: number;
|
|
32
|
+
uploaded_at: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface QueryDetailedResponse {
|
|
36
|
+
success: boolean;
|
|
37
|
+
stream_id: string;
|
|
38
|
+
file_count: number;
|
|
39
|
+
urls: string[];
|
|
40
|
+
files: FileInfo[];
|
|
41
|
+
expires_at: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface IngestResponse {
|
|
45
|
+
accepted: number;
|
|
46
|
+
buffered: number;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export class Enrich {
|
|
50
|
+
readonly apiKey: string;
|
|
51
|
+
readonly baseUrl: string;
|
|
52
|
+
readonly batchSize: number;
|
|
53
|
+
readonly flushInterval: number;
|
|
54
|
+
readonly maxRetries: number;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Create an Enrich client.
|
|
58
|
+
* @param apiKey Your API key (sk_live_* or sk_test_*)
|
|
59
|
+
* @param options Configuration options
|
|
60
|
+
*/
|
|
61
|
+
constructor(apiKey: string, options?: EnrichOptions);
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Send events immediately.
|
|
65
|
+
* @param streamId Target stream identifier
|
|
66
|
+
* @param data Single event or array of events
|
|
67
|
+
*/
|
|
68
|
+
ingest(streamId: string, data: Record<string, unknown> | Record<string, unknown>[]): Promise<IngestResponse>;
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Buffer a single event for batched delivery.
|
|
72
|
+
* Auto-flushes at batchSize or after flushInterval ms.
|
|
73
|
+
* @param streamId Target stream
|
|
74
|
+
* @param event Event payload (any JSON-serializable object)
|
|
75
|
+
*/
|
|
76
|
+
track(streamId: string, event: Record<string, unknown>): void;
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Flush buffered events.
|
|
80
|
+
* @param streamId Flush one stream, or omit to flush all
|
|
81
|
+
*/
|
|
82
|
+
flush(streamId?: string): Promise<void>;
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Get presigned Parquet file URLs for DuckDB.
|
|
86
|
+
* @param streamId Stream to query
|
|
87
|
+
* @param params Date filters
|
|
88
|
+
* @returns Array of signed URLs
|
|
89
|
+
*/
|
|
90
|
+
query(streamId: string, params?: QueryParams): Promise<string[]>;
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Get full query response with file metadata.
|
|
94
|
+
* @param streamId Stream to query
|
|
95
|
+
* @param params Date filters
|
|
96
|
+
*/
|
|
97
|
+
queryDetailed(streamId: string, params?: QueryParams): Promise<QueryDetailedResponse>;
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Best-effort flush that survives page unload/tab close.
|
|
101
|
+
* Uses fetch({ keepalive: true }) with proper Authorization headers.
|
|
102
|
+
* Fire-and-forget — does not return a Promise.
|
|
103
|
+
* @param streamId Flush one stream, or omit to flush all
|
|
104
|
+
*/
|
|
105
|
+
beacon(streamId?: string): void;
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Flush all buffers and stop timers. Call before shutdown.
|
|
109
|
+
*/
|
|
110
|
+
destroy(): Promise<void>;
|
|
111
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Enrich.sh SDK
|
|
3
|
+
* Official client for data ingestion and retrieval.
|
|
4
|
+
*
|
|
5
|
+
* @version 1.1.0
|
|
6
|
+
* @license MIT
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export class Enrich {
|
|
10
|
+
/**
|
|
11
|
+
* @param {string} apiKey - Your API key (sk_live_* or sk_test_*)
|
|
12
|
+
* @param {Object} [options]
|
|
13
|
+
* @param {string} [options.baseUrl=https://enrich.sh] - API base URL
|
|
14
|
+
* @param {number} [options.batchSize=100] - Events buffered before auto-flush
|
|
15
|
+
* @param {number} [options.flushInterval=5000] - Max ms between flushes
|
|
16
|
+
* @param {number} [options.maxRetries=2] - Retry count on flush failure
|
|
17
|
+
*/
|
|
18
|
+
constructor(apiKey, options = {}) {
|
|
19
|
+
if (!apiKey) {
|
|
20
|
+
throw new Error('Enrich: API key is required. Get one at dashboard.enrich.sh');
|
|
21
|
+
}
|
|
22
|
+
if (!apiKey.startsWith('sk_live_') && !apiKey.startsWith('sk_test_')) {
|
|
23
|
+
throw new Error('Enrich: Invalid key format. Keys start with sk_live_ or sk_test_');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
this.apiKey = apiKey;
|
|
27
|
+
this.baseUrl = (options.baseUrl || 'https://enrich.sh').replace(/\/+$/, '');
|
|
28
|
+
this.batchSize = options.batchSize || 100;
|
|
29
|
+
this.flushInterval = options.flushInterval || 5000;
|
|
30
|
+
this.maxRetries = options.maxRetries ?? 2;
|
|
31
|
+
|
|
32
|
+
this._buffers = new Map(); // streamId -> event[]
|
|
33
|
+
this._timers = new Map(); // streamId -> timerId
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// ── Ingestion ───────────────────────────────────────────────
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Validate stream ID format (must match backend rules).
|
|
40
|
+
* @param {string} streamId
|
|
41
|
+
*/
|
|
42
|
+
_validateStreamId(streamId) {
|
|
43
|
+
if (!streamId || typeof streamId !== 'string') {
|
|
44
|
+
throw new Error('Enrich: streamId is required and must be a string');
|
|
45
|
+
}
|
|
46
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(streamId)) {
|
|
47
|
+
throw new Error('Enrich: streamId must be alphanumeric (a-z, 0-9, _, -)');
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Send events immediately.
|
|
53
|
+
* @param {string} streamId - Target stream identifier
|
|
54
|
+
* @param {Object|Object[]} data - Event object or array of events
|
|
55
|
+
* @returns {Promise<Object>} API response
|
|
56
|
+
*/
|
|
57
|
+
async ingest(streamId, data) {
|
|
58
|
+
this._validateStreamId(streamId);
|
|
59
|
+
const events = Array.isArray(data) ? data : [data];
|
|
60
|
+
if (events.length === 0) return { success: true, events_received: 0 };
|
|
61
|
+
|
|
62
|
+
const res = await fetch(`${this.baseUrl}/ingest`, {
|
|
63
|
+
method: 'POST',
|
|
64
|
+
headers: {
|
|
65
|
+
'Authorization': `Bearer ${this.apiKey}`,
|
|
66
|
+
'Content-Type': 'application/json',
|
|
67
|
+
},
|
|
68
|
+
body: JSON.stringify({
|
|
69
|
+
stream_id: streamId,
|
|
70
|
+
data: events,
|
|
71
|
+
}),
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
if (!res.ok) {
|
|
75
|
+
const body = await res.json().catch(() => ({}));
|
|
76
|
+
throw new Error(body.error || `Enrich ingest failed (${res.status})`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return res.json();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Buffer a single event for automatic batched delivery.
|
|
84
|
+
* Flushes when buffer hits batchSize or after flushInterval ms.
|
|
85
|
+
* @param {string} streamId - Target stream
|
|
86
|
+
* @param {Object} event - Event payload
|
|
87
|
+
*/
|
|
88
|
+
track(streamId, event) {
|
|
89
|
+
this._validateStreamId(streamId);
|
|
90
|
+
if (!this._buffers.has(streamId)) {
|
|
91
|
+
this._buffers.set(streamId, []);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
this._buffers.get(streamId).push({
|
|
95
|
+
...event,
|
|
96
|
+
_ts: Date.now(),
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
this._resetTimer(streamId);
|
|
100
|
+
|
|
101
|
+
if (this._buffers.get(streamId).length >= this.batchSize) {
|
|
102
|
+
this.flush(streamId);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Flush buffered events for one stream, or all streams.
|
|
108
|
+
* @param {string} [streamId] - Omit to flush everything
|
|
109
|
+
* @returns {Promise}
|
|
110
|
+
*/
|
|
111
|
+
async flush(streamId) {
|
|
112
|
+
if (streamId) return this._flushOne(streamId);
|
|
113
|
+
return Promise.all(
|
|
114
|
+
Array.from(this._buffers.keys()).map((id) => this._flushOne(id)),
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ── Query ───────────────────────────────────────────────────
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Get presigned URLs for stored Parquet files.
|
|
122
|
+
* Pass these directly to DuckDB's read_parquet().
|
|
123
|
+
*
|
|
124
|
+
* @param {string} streamId
|
|
125
|
+
* @param {Object} [params]
|
|
126
|
+
* @param {string} [params.date] - Single day, YYYY-MM-DD
|
|
127
|
+
* @param {string} [params.start] - Range start, YYYY-MM-DD
|
|
128
|
+
* @param {string} [params.end] - Range end, YYYY-MM-DD
|
|
129
|
+
* @param {number} [params.days] - Last N days
|
|
130
|
+
* @returns {Promise<string[]>} Array of signed URLs
|
|
131
|
+
*/
|
|
132
|
+
async query(streamId, params = {}) {
|
|
133
|
+
const qs = new URLSearchParams({ stream_id: streamId, ...params });
|
|
134
|
+
|
|
135
|
+
const res = await fetch(`${this.baseUrl}/query-urls?${qs}`, {
|
|
136
|
+
headers: { 'Authorization': `Bearer ${this.apiKey}` },
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
if (!res.ok) {
|
|
140
|
+
const body = await res.json().catch(() => ({}));
|
|
141
|
+
throw new Error(body.error || `Enrich query failed (${res.status})`);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const data = await res.json();
|
|
145
|
+
return data.urls || [];
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Get full query response including file metadata.
|
|
150
|
+
* @param {string} streamId
|
|
151
|
+
* @param {Object} [params] - Same as query()
|
|
152
|
+
* @returns {Promise<Object>} { urls, files, file_count, expires_at }
|
|
153
|
+
*/
|
|
154
|
+
async queryDetailed(streamId, params = {}) {
|
|
155
|
+
const qs = new URLSearchParams({ stream_id: streamId, ...params });
|
|
156
|
+
|
|
157
|
+
const res = await fetch(`${this.baseUrl}/query-urls?${qs}`, {
|
|
158
|
+
headers: { 'Authorization': `Bearer ${this.apiKey}` },
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
if (!res.ok) {
|
|
162
|
+
const body = await res.json().catch(() => ({}));
|
|
163
|
+
throw new Error(body.error || `Enrich query failed (${res.status})`);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return res.json();
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// ── Lifecycle ───────────────────────────────────────────────
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Best-effort flush that survives page unload.
|
|
173
|
+
* Uses fetch({ keepalive: true }) — works like sendBeacon but
|
|
174
|
+
* supports Authorization headers (no token in URL).
|
|
175
|
+
* @param {string} [streamId] - Flush one stream, or omit for all
|
|
176
|
+
*/
|
|
177
|
+
beacon(streamId) {
|
|
178
|
+
const ids = streamId
|
|
179
|
+
? [streamId]
|
|
180
|
+
: Array.from(this._buffers.keys());
|
|
181
|
+
|
|
182
|
+
for (const id of ids) {
|
|
183
|
+
const buffer = this._buffers.get(id);
|
|
184
|
+
if (!buffer || buffer.length === 0) continue;
|
|
185
|
+
|
|
186
|
+
const events = buffer.splice(0, buffer.length);
|
|
187
|
+
this._clearTimer(id);
|
|
188
|
+
|
|
189
|
+
fetch(`${this.baseUrl}/ingest`, {
|
|
190
|
+
method: 'POST',
|
|
191
|
+
headers: {
|
|
192
|
+
'Authorization': `Bearer ${this.apiKey}`,
|
|
193
|
+
'Content-Type': 'application/json',
|
|
194
|
+
},
|
|
195
|
+
body: JSON.stringify({ stream_id: id, data: events }),
|
|
196
|
+
keepalive: true,
|
|
197
|
+
}).catch(() => { });
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Flush all buffers and stop timers. Call before shutdown.
|
|
203
|
+
*/
|
|
204
|
+
async destroy() {
|
|
205
|
+
try {
|
|
206
|
+
await this.flush();
|
|
207
|
+
} catch {
|
|
208
|
+
// Best effort — cleanup must still happen
|
|
209
|
+
}
|
|
210
|
+
for (const [id, timer] of this._timers) {
|
|
211
|
+
clearTimeout(timer);
|
|
212
|
+
}
|
|
213
|
+
this._timers.clear();
|
|
214
|
+
this._buffers.clear();
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// ── Private ─────────────────────────────────────────────────
|
|
218
|
+
|
|
219
|
+
async _flushOne(streamId) {
|
|
220
|
+
const buffer = this._buffers.get(streamId);
|
|
221
|
+
if (!buffer || buffer.length === 0) return;
|
|
222
|
+
|
|
223
|
+
const events = buffer.splice(0, buffer.length);
|
|
224
|
+
this._clearTimer(streamId);
|
|
225
|
+
|
|
226
|
+
let lastErr;
|
|
227
|
+
for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
|
|
228
|
+
try {
|
|
229
|
+
return await this.ingest(streamId, events);
|
|
230
|
+
} catch (err) {
|
|
231
|
+
lastErr = err;
|
|
232
|
+
if (attempt < this.maxRetries) {
|
|
233
|
+
await new Promise((r) => setTimeout(r, 500 * (attempt + 1)));
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
console.error(`[enrich] flush failed for "${streamId}" after ${this.maxRetries + 1} attempts:`, lastErr);
|
|
239
|
+
throw lastErr;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
_resetTimer(streamId) {
|
|
243
|
+
this._clearTimer(streamId);
|
|
244
|
+
const timer = setTimeout(() => this.flush(streamId), this.flushInterval);
|
|
245
|
+
if (typeof timer === 'object' && timer.unref) timer.unref();
|
|
246
|
+
this._timers.set(streamId, timer);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
_clearTimer(streamId) {
|
|
250
|
+
const timer = this._timers.get(streamId);
|
|
251
|
+
if (timer) {
|
|
252
|
+
clearTimeout(timer);
|
|
253
|
+
this._timers.delete(streamId);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|