@guidelytech/node 0.0.0-stage → 0.12.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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Guidely
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 CHANGED
@@ -1,3 +1,128 @@
1
- # Temporary Holding Version
1
+ # @guidelytech/node
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Send product data to [Guidely](https://guidelytech.com) from your Node.js backend: who a user is
4
+ (`identify`), what they did (`track`) and which account they belong to (`group`). Server events land
5
+ next to browser and app events, so they work in Events, Funnels, Retention, audiences and guide
6
+ targeting.
7
+
8
+ * Node.js 18+, no runtime dependencies (uses the global `fetch`). ESM and CommonJS.
9
+ * Calls never throw and never wait for the network: they validate, queue and return.
10
+ * Batched (100 messages, every 5 s), one request at a time, retried with exponential backoff and
11
+ `Retry-After`. Every message has a `messageId`, so retries never double-count.
12
+
13
+ ```bash
14
+ npm install @guidelytech/node
15
+ ```
16
+
17
+ Feature flags for servers live in [`@guidelytech/flags`](../flags).
18
+
19
+ ## Keys
20
+
21
+ Create a **secret key** (`gly_sk_…`) with the **`events:write`** scope in **Settings → API keys**.
22
+ Keep it in your secret manager; never ship it to a browser or app. Each client writes to one
23
+ environment: `development`, `staging` or `production`.
24
+
25
+ ## Usage
26
+
27
+ ```ts
28
+ import { Guidely } from "@guidelytech/node";
29
+
30
+ const guidely = new Guidely({
31
+ secretKey: process.env.GUIDELY_SECRET_KEY!, // gly_sk_…
32
+ environment: "production",
33
+ });
34
+
35
+ // Who the user is (merged into the user's traits; creates the user if needed).
36
+ guidely.identify({ userId: "u_42", traits: { email: "ada@example.com", plan: "pro" }, accountId: "acme" });
37
+
38
+ // What they did.
39
+ guidely.track({ userId: "u_42", event: "Report Exported", properties: { format: "csv", rows: 1200 } });
40
+
41
+ // A visitor who has not signed in: pass the browser SDK's Guidely.anonymousId.
42
+ guidely.track({ anonymousId: req.body.anonymousId, event: "Pricing Viewed" });
43
+
44
+ // Account traits (`name` also becomes the account name); userId adds the user to the account.
45
+ guidely.group({ accountId: "acme", traits: { name: "Acme Inc", mrr: 4200, seats: 25 }, userId: "u_42" });
46
+ ```
47
+
48
+ Use the same `userId` your frontend passes to `Guidely.identify()`. Every call accepts an optional
49
+ `timestamp` (a `Date`, epoch milliseconds or an ISO 8601 string) for events that happened earlier.
50
+
51
+ Create **one client per process** and share it. Before the process exits, call `shutdown()`: it
52
+ stops the timer and sends what is queued (waiting at most `shutdownTimeoutMs`).
53
+
54
+ ```ts
55
+ process.on("SIGTERM", async () => {
56
+ await guidely.shutdown();
57
+ process.exit(0);
58
+ });
59
+ ```
60
+
61
+ The client also flushes on Node's `beforeExit` event (when the event loop empties, e.g. at the end
62
+ of a script). `process.exit()` and signals skip `beforeExit`, so servers should call `shutdown()`.
63
+
64
+ ### Serverless (AWS Lambda, Vercel, Cloud Functions)
65
+
66
+ The function may be frozen as soon as it returns, so send before returning:
67
+
68
+ ```ts
69
+ export const handler = async (event) => {
70
+ guidely.track({ userId: event.userId, event: "Invoice Paid" });
71
+ await guidely.flush();
72
+ return { statusCode: 200 };
73
+ };
74
+ ```
75
+
76
+ ## Options
77
+
78
+ | Option | Default | |
79
+ |---|---|---|
80
+ | `secretKey` | — | **Required.** A `gly_sk_…` key with `events:write`. |
81
+ | `environment` | — | **Required.** `development`, `staging` or `production`. |
82
+ | `apiUrl` | `https://guidelytech.com` | For self-hosted or local servers. |
83
+ | `flushAt` | `100` | Send when this many messages are queued (a batch holds at most 100 messages and 500 KB). |
84
+ | `flushIntervalMs` | `5000` | Send what is queued at least this often. The timer never keeps the process alive. |
85
+ | `maxQueueSize` | `10000` | Messages held in memory; beyond it the oldest are dropped and reported. |
86
+ | `maxRetries` | `5` | Retries after network errors, timeouts, `429` and `5xx` (1 s, 2 s, 4 s… capped at 30 s, ±20 % jitter; `Retry-After` wins, capped at 60 s). Other `4xx` answers drop the batch. |
87
+ | `timeoutMs` | `10000` | Timeout of one request. |
88
+ | `shutdownTimeoutMs` | `10000` | How long `shutdown()` and the exit hook wait. |
89
+ | `onError` | `console.warn` | Receives a `GuidelyError` for every problem (see below). |
90
+ | `handleExit` | `true` | Flush on `beforeExit`. |
91
+ | `fetch` | global `fetch` | Any `fetch`-compatible function (proxies, tests). |
92
+
93
+ ## Validation and errors
94
+
95
+ Invalid calls are dropped and reported, never thrown: `identify` needs a `userId`; `track` needs an
96
+ `event` (1–120 characters) and a `userId` or `anonymousId`; `group` needs an `accountId`. Ids are
97
+ strings of 1–256 characters (integers are converted); `traits` and `properties` must be plain
98
+ objects. Messages over 32 KB are dropped.
99
+
100
+ `onError` receives a `GuidelyError` with a `code`:
101
+
102
+ | `code` | |
103
+ |---|---|
104
+ | `invalid_message` | A call was invalid and dropped. |
105
+ | `message_too_large` | A message serialized to more than 32 KB. |
106
+ | `queue_full` | The queue was full; `count` oldest messages were dropped. |
107
+ | `delivery_failed` | A batch of `count` messages was rejected (`status`: `400`, `401`/`403` = bad key or missing scope, …), failed after every retry (`status` `0` = unreachable) or was cut short by the shutdown timeout. |
108
+ | `closed` | A call was made after `shutdown()`. |
109
+
110
+ ```ts
111
+ const guidely = new Guidely({
112
+ secretKey: process.env.GUIDELY_SECRET_KEY!,
113
+ environment: "production",
114
+ onError: (err) => logger.warn({ code: err.code, count: err.count, status: err.status }, err.message),
115
+ });
116
+ ```
117
+
118
+ ## HTTP API
119
+
120
+ The client posts to `POST /api/v1/sdk/server/batch?environment=<env>` with
121
+ `Authorization: Bearer gly_sk_…`; see [docs/server-sdks.md](../../docs/server-sdks.md) to call it
122
+ from other languages. Its behaviour is pinned by the cross-language vectors in
123
+ [`sdks/conformance/track-vectors.json`](../../sdks/conformance/track-vectors.json), which the Java
124
+ and PHP SDKs run too (regenerate them with `node packages/node/scripts/generate-track-vectors.mjs`).
125
+
126
+ ## License
127
+
128
+ MIT
package/dist/index.cjs ADDED
@@ -0,0 +1,444 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ DEFAULT_API_URL: () => DEFAULT_API_URL,
24
+ Guidely: () => Guidely,
25
+ GuidelyError: () => GuidelyError,
26
+ SDK_NAME: () => SDK_NAME,
27
+ SDK_VERSION: () => SDK_VERSION
28
+ });
29
+ module.exports = __toCommonJS(index_exports);
30
+
31
+ // src/message.ts
32
+ var import_node_crypto = require("crypto");
33
+ var LIMITS = {
34
+ /** Most messages in one request (the API accepts 1–100). */
35
+ maxBatchMessages: 100,
36
+ /** Most serialized message bytes in one request (the API accepts bodies up to 512 KB). */
37
+ maxBatchBytes: 5e5,
38
+ /** A message larger than this when serialized is dropped and reported. */
39
+ maxMessageBytes: 32768,
40
+ maxEventLength: 120,
41
+ maxIdLength: 256
42
+ };
43
+ var InvalidMessageError = class extends Error {
44
+ };
45
+ var ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
46
+ function newMessageId() {
47
+ let id2 = "";
48
+ while (id2.length < 24) {
49
+ for (const b of (0, import_node_crypto.randomBytes)(32)) {
50
+ if (b < 248) id2 += ALPHABET[b % 62];
51
+ if (id2.length === 24) break;
52
+ }
53
+ }
54
+ return id2;
55
+ }
56
+ function formatTimestamp(value) {
57
+ let date;
58
+ if (value instanceof Date) date = value;
59
+ else if (typeof value === "number") date = new Date(value);
60
+ else if (typeof value === "string" && /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(:\d{2}(\.\d+)?)?(Z|[+-]\d{2}:?\d{2})$/i.test(value.trim())) date = new Date(value.trim());
61
+ else throw new InvalidMessageError("timestamp must be a Date, epoch milliseconds or an ISO 8601 string with an offset");
62
+ if (Number.isNaN(date.getTime())) throw new InvalidMessageError("timestamp is not a valid date");
63
+ return date.toISOString();
64
+ }
65
+ var length = (s) => s.length;
66
+ function id(name, value, required) {
67
+ if (value === void 0 || value === null || !required && value === "") {
68
+ if (required) throw new InvalidMessageError(`${name} is required`);
69
+ return void 0;
70
+ }
71
+ let s;
72
+ if (typeof value === "string") s = value;
73
+ else if (typeof value === "number" && Number.isSafeInteger(value)) s = String(value);
74
+ else throw new InvalidMessageError(`${name} must be a string`);
75
+ if (s.trim() === "") throw new InvalidMessageError(`${name} must not be empty`);
76
+ if (length(s) > LIMITS.maxIdLength) throw new InvalidMessageError(`${name} is longer than ${LIMITS.maxIdLength} characters`);
77
+ return s;
78
+ }
79
+ function isPlainObject(v) {
80
+ if (typeof v !== "object" || v === null || Array.isArray(v)) return false;
81
+ const proto = Object.getPrototypeOf(v);
82
+ return proto === Object.prototype || proto === null;
83
+ }
84
+ function object(name, value) {
85
+ if (value === void 0 || value === null) return void 0;
86
+ if (!isPlainObject(value)) throw new InvalidMessageError(`${name} must be an object`);
87
+ return value;
88
+ }
89
+ function buildMessage(type, args, now = () => /* @__PURE__ */ new Date()) {
90
+ if (!isPlainObject(args)) throw new InvalidMessageError(`${type}() needs an object argument`);
91
+ const timestamp = args.timestamp === void 0 || args.timestamp === null ? now().toISOString() : formatTimestamp(args.timestamp);
92
+ const base = { type, messageId: newMessageId(), timestamp };
93
+ switch (type) {
94
+ case "identify": {
95
+ const userId = id("userId", args.userId, true);
96
+ const traits = object("traits", args.traits);
97
+ const accountId = id("accountId", args.accountId, false);
98
+ return { ...base, userId, ...traits ? { traits } : {}, ...accountId ? { accountId } : {} };
99
+ }
100
+ case "track": {
101
+ const userId = id("userId", args.userId, false);
102
+ const anonymousId = id("anonymousId", args.anonymousId, false);
103
+ if (!userId && !anonymousId) throw new InvalidMessageError("track() needs a userId or an anonymousId");
104
+ const event = args.event;
105
+ if (typeof event !== "string" || event.trim() === "") throw new InvalidMessageError("track() needs an event name");
106
+ if (length(event) > LIMITS.maxEventLength) throw new InvalidMessageError(`event is longer than ${LIMITS.maxEventLength} characters`);
107
+ const properties = object("properties", args.properties);
108
+ const accountId = id("accountId", args.accountId, false);
109
+ return {
110
+ ...base,
111
+ ...userId ? { userId } : {},
112
+ ...anonymousId ? { anonymousId } : {},
113
+ event,
114
+ ...properties ? { properties } : {},
115
+ ...accountId ? { accountId } : {}
116
+ };
117
+ }
118
+ case "group": {
119
+ const accountId = id("accountId", args.accountId, true);
120
+ const traits = object("traits", args.traits);
121
+ const userId = id("userId", args.userId, false);
122
+ return { ...base, accountId, ...traits ? { traits } : {}, ...userId ? { userId } : {} };
123
+ }
124
+ }
125
+ }
126
+
127
+ // src/retry.ts
128
+ var DEFAULT_BACKOFF = { baseMs: 1e3, maxMs: 3e4, retryAfterMaxMs: 6e4 };
129
+ function responseAction(status) {
130
+ if (status >= 200 && status < 300) return "ok";
131
+ if (status === 0 || status === 429 || status >= 500 && status < 600) return "retry";
132
+ return "drop";
133
+ }
134
+ function parseRetryAfter(value) {
135
+ if (value == null) return null;
136
+ const v = value.trim();
137
+ return /^\d+$/.test(v) ? Number(v) * 1e3 : null;
138
+ }
139
+ function backoffDelay(attempt, retryAfter, o = DEFAULT_BACKOFF) {
140
+ const ra = parseRetryAfter(retryAfter);
141
+ if (ra !== null) return Math.min(ra, o.retryAfterMaxMs);
142
+ return Math.min(o.baseMs * 2 ** Math.max(0, attempt - 1), o.maxMs);
143
+ }
144
+ function jitter(delayMs, random) {
145
+ return Math.round(delayMs * (0.8 + 0.4 * random));
146
+ }
147
+
148
+ // src/client.ts
149
+ var SDK_NAME = "guidely-node";
150
+ var SDK_VERSION = "0.12.0";
151
+ var DEFAULT_API_URL = "https://guidelytech.com";
152
+ var BATCH_PATH = "/api/v1/sdk/server/batch";
153
+ var ENVIRONMENTS = ["development", "staging", "production"];
154
+ var GuidelyError = class extends Error {
155
+ constructor(code, message, count = 1, status, options) {
156
+ super(message, options);
157
+ this.code = code;
158
+ this.count = count;
159
+ this.status = status;
160
+ }
161
+ code;
162
+ count;
163
+ status;
164
+ name = "GuidelyError";
165
+ };
166
+ var int = (value, fallback, min) => typeof value === "number" && Number.isFinite(value) ? Math.max(min, Math.floor(value)) : fallback;
167
+ var Guidely = class {
168
+ url;
169
+ headers;
170
+ flushAt;
171
+ flushIntervalMs;
172
+ maxQueueSize;
173
+ maxRetries;
174
+ shutdownTimeoutMs;
175
+ timeoutMs;
176
+ onError;
177
+ fetch;
178
+ sdkJson = JSON.stringify({ name: SDK_NAME, version: SDK_VERSION });
179
+ queue = [];
180
+ droppedSinceReport = 0;
181
+ timer;
182
+ pumping = null;
183
+ drainAll = false;
184
+ /** Bumped to abandon the running pump (shutdown timeout): its request is aborted and its sleeps end. */
185
+ epoch = 0;
186
+ inflight = null;
187
+ sleeps = /* @__PURE__ */ new Set();
188
+ closed = false;
189
+ shutdownPromise = null;
190
+ exitDrain = null;
191
+ exitListener = null;
192
+ constructor(options) {
193
+ const o = options ?? {};
194
+ const key = typeof o.secretKey === "string" ? o.secretKey.trim() : "";
195
+ if (!key.startsWith("gly_sk_")) {
196
+ throw new TypeError("Guidely: secretKey must be a secret API key (gly_sk_\u2026) with the events:write scope");
197
+ }
198
+ if (!ENVIRONMENTS.includes(o.environment)) {
199
+ throw new TypeError('Guidely: environment must be "development", "staging" or "production"');
200
+ }
201
+ const fetchImpl = o.fetch ?? (typeof globalThis.fetch === "function" ? globalThis.fetch.bind(globalThis) : void 0);
202
+ if (!fetchImpl) throw new TypeError("Guidely: no global fetch (Node 18+ is required), pass the fetch option");
203
+ this.fetch = fetchImpl;
204
+ const apiUrl = (o.apiUrl ?? DEFAULT_API_URL).trim().replace(/\/+$/, "");
205
+ this.url = `${apiUrl}${BATCH_PATH}?environment=${encodeURIComponent(o.environment)}`;
206
+ this.headers = {
207
+ Authorization: `Bearer ${key}`,
208
+ "Content-Type": "application/json",
209
+ Accept: "application/json",
210
+ "User-Agent": `${SDK_NAME}/${SDK_VERSION} (Node ${typeof process !== "undefined" ? process.version : "unknown"})`
211
+ };
212
+ this.flushAt = int(o.flushAt, 100, 1);
213
+ this.flushIntervalMs = int(o.flushIntervalMs, 5e3, 1);
214
+ this.maxQueueSize = int(o.maxQueueSize, 1e4, 1);
215
+ this.maxRetries = int(o.maxRetries, 5, 0);
216
+ this.shutdownTimeoutMs = int(o.shutdownTimeoutMs, 1e4, 0);
217
+ this.timeoutMs = int(o.timeoutMs, 1e4, 1);
218
+ this.onError = o.onError ?? ((e) => console.warn(`[${SDK_NAME}] ${e.message}`));
219
+ if (o.handleExit !== false && typeof process !== "undefined" && typeof process.on === "function") {
220
+ this.exitListener = () => this.onBeforeExit();
221
+ process.on("beforeExit", this.exitListener);
222
+ }
223
+ }
224
+ /** Who a user is: merges `traits` into the user (creating it if needed), optionally linking an account. */
225
+ identify(message) {
226
+ this.enqueue("identify", message);
227
+ }
228
+ /** What a user (or anonymous visitor) did. */
229
+ track(message) {
230
+ this.enqueue("track", message);
231
+ }
232
+ /** Account traits, and optionally adds `userId` to the account. */
233
+ group(message) {
234
+ this.enqueue("group", message);
235
+ }
236
+ /** Sends everything queued; resolves once it has been delivered or dropped. Never rejects. */
237
+ flush() {
238
+ return this.startPump(true);
239
+ }
240
+ /**
241
+ * Stops the timer and the exit hook, then flushes, waiting at most `shutdownTimeoutMs`. Later calls
242
+ * are dropped (and reported). Call it before your process exits. Never rejects.
243
+ */
244
+ shutdown() {
245
+ if (this.shutdownPromise) return this.shutdownPromise;
246
+ this.closed = true;
247
+ this.clearTimer();
248
+ if (this.exitListener) process.removeListener("beforeExit", this.exitListener);
249
+ this.shutdownPromise = this.drain(this.shutdownTimeoutMs);
250
+ return this.shutdownPromise;
251
+ }
252
+ // --- queueing ------------------------------------------------------------------------------
253
+ enqueue(type, args) {
254
+ try {
255
+ if (this.closed) {
256
+ this.report(new GuidelyError("closed", `${type}() was called after shutdown(); the message was dropped`));
257
+ return;
258
+ }
259
+ const message = buildMessage(type, args);
260
+ let json;
261
+ try {
262
+ json = JSON.stringify(message);
263
+ } catch (e) {
264
+ throw new InvalidMessageError(`could not be serialized to JSON (${e instanceof Error ? e.message : String(e)})`);
265
+ }
266
+ const bytes = Buffer.byteLength(json, "utf8");
267
+ if (bytes > LIMITS.maxMessageBytes) {
268
+ this.report(new GuidelyError("message_too_large", `${type}() message is ${bytes} bytes, more than the ${LIMITS.maxMessageBytes}-byte limit; it was dropped`));
269
+ return;
270
+ }
271
+ if (this.queue.length >= this.maxQueueSize) {
272
+ this.queue.shift();
273
+ this.droppedSinceReport++;
274
+ }
275
+ this.queue.push({ json, bytes });
276
+ if (this.queue.length >= this.flushAt) void this.startPump(false);
277
+ else this.scheduleTimer();
278
+ } catch (e) {
279
+ if (e instanceof InvalidMessageError) this.report(new GuidelyError("invalid_message", `Invalid ${type}() call dropped: ${e.message}`));
280
+ else this.report(new GuidelyError("invalid_message", `${type}() failed: ${e instanceof Error ? e.message : String(e)}`, 1, void 0, { cause: e }));
281
+ }
282
+ }
283
+ scheduleTimer() {
284
+ if (this.timer !== void 0 || this.closed) return;
285
+ this.timer = setTimeout(() => {
286
+ this.timer = void 0;
287
+ void this.startPump(true);
288
+ }, this.flushIntervalMs);
289
+ this.timer.unref?.();
290
+ }
291
+ clearTimer() {
292
+ if (this.timer !== void 0) clearTimeout(this.timer);
293
+ this.timer = void 0;
294
+ }
295
+ // --- sending -------------------------------------------------------------------------------
296
+ /** Starts (or joins) the single sender. `all`: send everything, not only full batches. */
297
+ startPump(all) {
298
+ if (all) this.drainAll = true;
299
+ if (!this.pumping) this.pumping = this.pump();
300
+ return this.pumping;
301
+ }
302
+ async pump() {
303
+ await Promise.resolve();
304
+ const epoch = this.epoch;
305
+ try {
306
+ const cap = Math.min(this.flushAt, LIMITS.maxBatchMessages);
307
+ for (; ; ) {
308
+ this.reportDrops();
309
+ if (epoch !== this.epoch || this.queue.length === 0 || !this.drainAll && this.queue.length < cap) break;
310
+ await this.deliver(this.takeBatch(cap), epoch);
311
+ }
312
+ } catch (e) {
313
+ this.report(new GuidelyError("delivery_failed", `Unexpected error while sending: ${e instanceof Error ? e.message : String(e)}`, 0, void 0, { cause: e }));
314
+ } finally {
315
+ this.pumping = null;
316
+ this.drainAll = false;
317
+ if (this.queue.length > 0) this.scheduleTimer();
318
+ }
319
+ }
320
+ /** Up to `cap` messages from the head of the queue, at most `maxBatchBytes` of them (at least one). */
321
+ takeBatch(cap) {
322
+ let n = 0;
323
+ let size = 0;
324
+ while (n < this.queue.length && n < cap) {
325
+ const next = this.queue[n];
326
+ if (n > 0 && size + next.bytes > LIMITS.maxBatchBytes) break;
327
+ size += next.bytes;
328
+ n++;
329
+ }
330
+ return this.queue.splice(0, n);
331
+ }
332
+ async deliver(batch, epoch) {
333
+ const messages = batch.map((m) => m.json).join(",");
334
+ for (let attempt = 1; ; attempt++) {
335
+ if (epoch !== this.epoch) {
336
+ this.report(new GuidelyError("delivery_failed", `${batch.length} messages were not sent before the shutdown timeout`, batch.length, 0));
337
+ return;
338
+ }
339
+ const body = `{"batch":[${messages}],"sentAt":${JSON.stringify((/* @__PURE__ */ new Date()).toISOString())},"sdk":${this.sdkJson}}`;
340
+ let status = 0;
341
+ let retryAfter = null;
342
+ let failure;
343
+ try {
344
+ const res = await this.send(body);
345
+ status = res.status;
346
+ retryAfter = res.headers.get("retry-after");
347
+ } catch (e) {
348
+ failure = e;
349
+ }
350
+ const action = responseAction(status);
351
+ if (action === "ok") return;
352
+ if (epoch !== this.epoch) continue;
353
+ if (action === "drop" || attempt > this.maxRetries) {
354
+ this.report(new GuidelyError("delivery_failed", failureMessage(status, failure, batch.length, attempt), batch.length, status, { cause: failure }));
355
+ return;
356
+ }
357
+ const delay = parseRetryAfter(retryAfter) !== null ? backoffDelay(attempt, retryAfter) : jitter(backoffDelay(attempt, null, DEFAULT_BACKOFF), Math.random());
358
+ await this.sleep(delay);
359
+ }
360
+ }
361
+ async send(body) {
362
+ const controller = new AbortController();
363
+ this.inflight = controller;
364
+ const timeout = setTimeout(() => controller.abort(new Error(`request timed out after ${this.timeoutMs} ms`)), this.timeoutMs);
365
+ try {
366
+ const res = await this.fetch(this.url, { method: "POST", headers: this.headers, body, signal: controller.signal });
367
+ try {
368
+ await res.text?.();
369
+ } catch {
370
+ }
371
+ return res;
372
+ } finally {
373
+ clearTimeout(timeout);
374
+ if (this.inflight === controller) this.inflight = null;
375
+ }
376
+ }
377
+ sleep(ms) {
378
+ return new Promise((resolve) => {
379
+ const s = { timer: void 0, resolve };
380
+ s.timer = setTimeout(() => {
381
+ this.sleeps.delete(s);
382
+ resolve();
383
+ }, ms);
384
+ this.sleeps.add(s);
385
+ });
386
+ }
387
+ /** Flushes, giving up after `timeoutMs`: the request in flight is aborted and what is left is dropped. */
388
+ async drain(timeoutMs) {
389
+ let timer;
390
+ const timedOut = await Promise.race([
391
+ this.startPump(true).then(() => false),
392
+ new Promise((resolve) => {
393
+ timer = setTimeout(() => resolve(true), timeoutMs);
394
+ })
395
+ ]);
396
+ clearTimeout(timer);
397
+ if (!timedOut) return;
398
+ this.epoch++;
399
+ this.inflight?.abort(new Error("shutdown timeout"));
400
+ for (const s of this.sleeps) {
401
+ clearTimeout(s.timer);
402
+ s.resolve();
403
+ }
404
+ this.sleeps.clear();
405
+ const left = this.queue.length;
406
+ this.queue = [];
407
+ if (left > 0) this.report(new GuidelyError("delivery_failed", `${left} queued messages were not sent within ${timeoutMs} ms`, left, 0));
408
+ }
409
+ onBeforeExit() {
410
+ if (this.closed || this.exitDrain || this.queue.length === 0 && !this.pumping) return;
411
+ this.exitDrain = this.drain(this.shutdownTimeoutMs).finally(() => {
412
+ this.exitDrain = null;
413
+ });
414
+ }
415
+ // --- errors --------------------------------------------------------------------------------
416
+ reportDrops() {
417
+ if (this.droppedSinceReport === 0) return;
418
+ const n = this.droppedSinceReport;
419
+ this.droppedSinceReport = 0;
420
+ this.report(new GuidelyError("queue_full", `The queue was full (maxQueueSize ${this.maxQueueSize}): dropped the ${n} oldest messages`, n));
421
+ }
422
+ report(error) {
423
+ try {
424
+ this.onError(error);
425
+ } catch {
426
+ }
427
+ }
428
+ };
429
+ function failureMessage(status, failure, count, attempts) {
430
+ const what = `${count} messages were dropped`;
431
+ if (status === 0) return `${what}: Guidely could not be reached in ${attempts} attempt${attempts === 1 ? "" : "s"} (${failure instanceof Error ? failure.message : String(failure)})`;
432
+ if (status === 401 || status === 403) return `${what}: the key was rejected (HTTP ${status}). Check it is a gly_sk_ key with the events:write scope for this environment`;
433
+ if (attempts > 1) return `${what}: HTTP ${status} after ${attempts} attempts`;
434
+ return `${what}: HTTP ${status}`;
435
+ }
436
+ // Annotate the CommonJS export names for ESM import in node:
437
+ 0 && (module.exports = {
438
+ DEFAULT_API_URL,
439
+ Guidely,
440
+ GuidelyError,
441
+ SDK_NAME,
442
+ SDK_VERSION
443
+ });
444
+ //# sourceMappingURL=index.cjs.map