@chaja/sdk-node 0.0.0-stage → 0.1.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/README.md +18 -2
- package/dist/index.d.mts +162 -0
- package/dist/index.mjs +414 -0
- package/package.json +47 -3
package/README.md
CHANGED
|
@@ -1,3 +1,19 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @chaja/sdk-node
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Error tracking for Node.js 22+, reporting to a self-hosted [Chajá](https://github.com/emiliodominguez/chaja) server.
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
pnpm add @chaja/sdk-node
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import { expressErrorHandler, init } from "@chaja/sdk-node";
|
|
11
|
+
|
|
12
|
+
init({ endpoint: process.env.CHAJA_URL!, key: process.env.CHAJA_KEY!, service: "api", release: process.env.RELEASE });
|
|
13
|
+
|
|
14
|
+
app.use(expressErrorHandler());
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Uncaught exceptions and unhandled rejections are reported with source context. Events that cannot be delivered can be kept on
|
|
18
|
+
disk (`offlineDirectory`) and resent on the next start. For Hono, Next.js route handlers and other Fetch-API servers, use
|
|
19
|
+
`captureRequestError(error, request)`. All options: [docs/sdk.md](https://github.com/emiliodominguez/chaja/blob/main/docs/sdk.md).
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { BreadcrumbInput, CaptureContext, CaptureContext as CaptureContext$1, Client, Client as Client$1, ClientOptions, ClientOptions as ClientOptions$1 } from "@chaja/sdk-core";
|
|
2
|
+
//#region ../protocol/src/wire.d.ts
|
|
3
|
+
type AttributeScalar = string | number | boolean;
|
|
4
|
+
type AttributeValue = AttributeScalar | AttributeScalar[];
|
|
5
|
+
type Attributes = Record<string, AttributeValue>;
|
|
6
|
+
interface RequestInfo {
|
|
7
|
+
url?: string;
|
|
8
|
+
method?: string;
|
|
9
|
+
headers?: Record<string, string>;
|
|
10
|
+
}
|
|
11
|
+
//#endregion
|
|
12
|
+
//#region src/index.d.ts
|
|
13
|
+
export interface NodeOptions extends ClientOptions$1 {
|
|
14
|
+
/**
|
|
15
|
+
* Capture `uncaughtException` and `unhandledRejection`. Default true.
|
|
16
|
+
*/
|
|
17
|
+
processHandlers?: boolean;
|
|
18
|
+
/**
|
|
19
|
+
* Exit with code 1 after reporting an uncaught exception (Node's default behaviour). Default true.
|
|
20
|
+
*/
|
|
21
|
+
exitOnUncaught?: boolean;
|
|
22
|
+
/**
|
|
23
|
+
* Directory where envelopes that could not be delivered are kept and retried on the next start.
|
|
24
|
+
*/
|
|
25
|
+
offlineDirectory?: string;
|
|
26
|
+
/**
|
|
27
|
+
* Lines of source code around in-app frames. Default 5; 0 disables.
|
|
28
|
+
*/
|
|
29
|
+
contextLines?: number;
|
|
30
|
+
/**
|
|
31
|
+
* HTTP client (tests inject one). Defaults to global fetch.
|
|
32
|
+
*/
|
|
33
|
+
fetch?: typeof fetch;
|
|
34
|
+
}
|
|
35
|
+
export interface NodeClient extends Client$1 {
|
|
36
|
+
close(timeoutMs?: number): Promise<boolean>;
|
|
37
|
+
/**
|
|
38
|
+
* The HTTP client the SDK sends with.
|
|
39
|
+
*/
|
|
40
|
+
readonly fetch: typeof fetch;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Start the Node SDK.
|
|
44
|
+
*
|
|
45
|
+
* @param options - SDK options.
|
|
46
|
+
* @returns The client; calling `init` again replaces it.
|
|
47
|
+
*/
|
|
48
|
+
export declare function init(options: NodeOptions): NodeClient;
|
|
49
|
+
/**
|
|
50
|
+
* The parts of an Express, Fastify or `http.IncomingMessage` request the SDK reads.
|
|
51
|
+
*/
|
|
52
|
+
export interface NodeRequestLike {
|
|
53
|
+
method?: string;
|
|
54
|
+
url?: string;
|
|
55
|
+
originalUrl?: string;
|
|
56
|
+
headers?: Record<string, string | string[] | undefined>;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Request details from a Node-style request object, without the body.
|
|
60
|
+
*
|
|
61
|
+
* @param request - Incoming request.
|
|
62
|
+
* @returns Request info.
|
|
63
|
+
*/
|
|
64
|
+
export declare function requestInfo(request: NodeRequestLike): RequestInfo;
|
|
65
|
+
export type ExpressErrorHandler = (error: unknown, request: NodeRequestLike, response: unknown, next: (error?: unknown) => void) => void;
|
|
66
|
+
/**
|
|
67
|
+
* Express error middleware: reports the error with request details, then passes it on.
|
|
68
|
+
*
|
|
69
|
+
* @param client - Client; defaults to the one from `init`.
|
|
70
|
+
* @returns Express error handler.
|
|
71
|
+
*/
|
|
72
|
+
export declare function expressErrorHandler(client?: Client$1): ExpressErrorHandler;
|
|
73
|
+
/**
|
|
74
|
+
* Report an error from a Fetch-API style request (Hono, Next.js route handlers, Bun, Deno).
|
|
75
|
+
*
|
|
76
|
+
* @param error - Thrown value.
|
|
77
|
+
* @param request - Web request.
|
|
78
|
+
* @param context - Extra context.
|
|
79
|
+
* @returns Event id, if sent.
|
|
80
|
+
*/
|
|
81
|
+
export declare function captureRequestError(error: unknown, request: Request, context?: CaptureContext$1): string | undefined;
|
|
82
|
+
/**
|
|
83
|
+
* Capture an error with the client from `init`.
|
|
84
|
+
*
|
|
85
|
+
* @param error - Thrown value.
|
|
86
|
+
* @param context - Extra context.
|
|
87
|
+
* @returns Event id, if sent.
|
|
88
|
+
*/
|
|
89
|
+
export declare function captureException(error: unknown, context?: CaptureContext$1): string | undefined;
|
|
90
|
+
/**
|
|
91
|
+
* Capture a message with the client from `init`.
|
|
92
|
+
*
|
|
93
|
+
* @param message - Message.
|
|
94
|
+
* @param level - Level.
|
|
95
|
+
* @param context - Extra context.
|
|
96
|
+
* @returns Event id, if sent.
|
|
97
|
+
*/
|
|
98
|
+
export declare function captureMessage(message: string, level?: Parameters<Client$1["captureMessage"]>[1], context?: CaptureContext$1): string | undefined;
|
|
99
|
+
export interface ServerEvent {
|
|
100
|
+
/**
|
|
101
|
+
* Your id for the person who did it, the same one the browser SDK passes to `identify`.
|
|
102
|
+
*/
|
|
103
|
+
distinctId: string;
|
|
104
|
+
/**
|
|
105
|
+
* Event name, such as `subscription_renewed`.
|
|
106
|
+
*/
|
|
107
|
+
name: string;
|
|
108
|
+
properties?: Attributes;
|
|
109
|
+
/**
|
|
110
|
+
* Person properties to set.
|
|
111
|
+
*/
|
|
112
|
+
set?: Attributes;
|
|
113
|
+
/**
|
|
114
|
+
* Person properties to set only when the person does not have them yet.
|
|
115
|
+
*/
|
|
116
|
+
setOnce?: Attributes;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Record a product analytics event for a person with the client from `init`. Servers act for many people, so every event
|
|
120
|
+
* names its person.
|
|
121
|
+
*
|
|
122
|
+
* @param event - Person, name and properties.
|
|
123
|
+
* @returns Event id, if sent.
|
|
124
|
+
* @example
|
|
125
|
+
* capture({ distinctId: user.id, name: "subscription_renewed", properties: { plan: "pro" } });
|
|
126
|
+
*/
|
|
127
|
+
export declare function capture(event: ServerEvent): string | undefined;
|
|
128
|
+
/**
|
|
129
|
+
* Send queued events.
|
|
130
|
+
*
|
|
131
|
+
* @param timeoutMs - Maximum wait.
|
|
132
|
+
* @returns Whether everything was sent.
|
|
133
|
+
*/
|
|
134
|
+
export declare function flush(timeoutMs?: number): Promise<boolean>;
|
|
135
|
+
/**
|
|
136
|
+
* The client created by `init`, if any.
|
|
137
|
+
*
|
|
138
|
+
* @returns Current client.
|
|
139
|
+
*/
|
|
140
|
+
export declare function getClient(): NodeClient | undefined;
|
|
141
|
+
/**
|
|
142
|
+
* Feature flags for a person, with the client from `init`, cached for 30 seconds per person and properties.
|
|
143
|
+
*
|
|
144
|
+
* @param distinctId - Your id for the person.
|
|
145
|
+
* @param properties - Properties the flags' conditions may test, on top of what product analytics knows about them.
|
|
146
|
+
* @returns Flag values: `false`, `true` or a variant key; empty when the server cannot be reached.
|
|
147
|
+
* @example
|
|
148
|
+
* const flags = await getFeatureFlags(user.id, { plan: user.plan });
|
|
149
|
+
* if (flags["new-checkout"]) { ... }
|
|
150
|
+
*/
|
|
151
|
+
export declare function getFeatureFlags(distinctId: string, properties?: Attributes): Promise<Record<string, boolean | string>>;
|
|
152
|
+
/**
|
|
153
|
+
* Whether a flag is on for a person (a variant counts as on), with the client from `init`.
|
|
154
|
+
*
|
|
155
|
+
* @param key - Flag key.
|
|
156
|
+
* @param distinctId - Your id for the person.
|
|
157
|
+
* @param properties - Properties for the flag's conditions.
|
|
158
|
+
* @returns True when on.
|
|
159
|
+
*/
|
|
160
|
+
export declare function isFeatureEnabled(key: string, distinctId: string, properties?: Attributes): Promise<boolean>;
|
|
161
|
+
//#endregion
|
|
162
|
+
export type { BreadcrumbInput, CaptureContext, Client, ClientOptions };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { SDK_VERSION, createClient, parseRetryAfter } from "@chaja/sdk-core";
|
|
6
|
+
//#region src/index.ts
|
|
7
|
+
const SOURCE_CACHE_LIMIT = 100;
|
|
8
|
+
const MAX_LINE = 300;
|
|
9
|
+
let current;
|
|
10
|
+
/**
|
|
11
|
+
* Shorten long source lines.
|
|
12
|
+
*
|
|
13
|
+
* @param text - Source line.
|
|
14
|
+
* @returns Clipped line.
|
|
15
|
+
*/
|
|
16
|
+
function clip(text) {
|
|
17
|
+
return text.slice(0, MAX_LINE);
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Whether a scheduled handle is a Node timer.
|
|
21
|
+
*
|
|
22
|
+
* @param handle - Handle from `schedule`.
|
|
23
|
+
* @returns True for timers.
|
|
24
|
+
*/
|
|
25
|
+
function isTimer(handle) {
|
|
26
|
+
return typeof handle === "object" && handle !== null && "unref" in handle;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Read source lines for frames, caching files and skipping dependencies.
|
|
30
|
+
*
|
|
31
|
+
* @param lines - Context lines per side.
|
|
32
|
+
* @returns Frame enricher.
|
|
33
|
+
*/
|
|
34
|
+
function sourceContext(lines) {
|
|
35
|
+
const cache = /* @__PURE__ */ new Map();
|
|
36
|
+
/**
|
|
37
|
+
* Lines of a file, or null when unreadable.
|
|
38
|
+
*
|
|
39
|
+
* @param filename - Absolute path or file URL.
|
|
40
|
+
* @returns Lines.
|
|
41
|
+
*/
|
|
42
|
+
function read(filename) {
|
|
43
|
+
if (cache.has(filename)) return cache.get(filename) ?? null;
|
|
44
|
+
let content = null;
|
|
45
|
+
try {
|
|
46
|
+
const path = filename.startsWith("file://") ? fileURLToPath(filename) : filename;
|
|
47
|
+
content = readFileSync(path, "utf8").split(/\r?\n/u);
|
|
48
|
+
} catch {
|
|
49
|
+
content = null;
|
|
50
|
+
}
|
|
51
|
+
if (cache.size >= SOURCE_CACHE_LIMIT) cache.clear();
|
|
52
|
+
cache.set(filename, content);
|
|
53
|
+
return content;
|
|
54
|
+
}
|
|
55
|
+
return function withContext(frame) {
|
|
56
|
+
const { filename, lineno } = frame;
|
|
57
|
+
if (lines <= 0 || !filename || !lineno || filename.includes("node_modules") || filename.startsWith("node:") || !/^(?:\/|file:\/\/|[a-z]:\\)/iu.test(filename)) return frame;
|
|
58
|
+
const source = read(filename);
|
|
59
|
+
const line = source?.[lineno - 1];
|
|
60
|
+
if (!source || line === void 0) return frame;
|
|
61
|
+
return {
|
|
62
|
+
...frame,
|
|
63
|
+
contextLine: clip(line),
|
|
64
|
+
preContext: source.slice(Math.max(0, lineno - 1 - lines), lineno - 1).map(clip),
|
|
65
|
+
postContext: source.slice(lineno, lineno + lines).map(clip)
|
|
66
|
+
};
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Whether a frame is application code.
|
|
71
|
+
*
|
|
72
|
+
* @param frame - Frame.
|
|
73
|
+
* @returns In-app flag.
|
|
74
|
+
*/
|
|
75
|
+
function inApp(frame) {
|
|
76
|
+
const filename = frame.filename ?? "";
|
|
77
|
+
return !filename.includes("node_modules") && !filename.startsWith("node:") && !filename.startsWith("internal/");
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Start the Node SDK.
|
|
81
|
+
*
|
|
82
|
+
* @param options - SDK options.
|
|
83
|
+
* @returns The client; calling `init` again replaces it.
|
|
84
|
+
*/
|
|
85
|
+
function init(options) {
|
|
86
|
+
current?.close(0);
|
|
87
|
+
const send = options.fetch ?? fetch;
|
|
88
|
+
const envelopeUrl = `${options.endpoint.replace(/\/+$/u, "")}/api/v1/envelope`;
|
|
89
|
+
const withContext = sourceContext(options.contextLines ?? 5);
|
|
90
|
+
const cleanups = [];
|
|
91
|
+
const transport = { async send(body) {
|
|
92
|
+
try {
|
|
93
|
+
const response = await send(envelopeUrl, {
|
|
94
|
+
method: "POST",
|
|
95
|
+
headers: {
|
|
96
|
+
"content-type": "application/json",
|
|
97
|
+
"x-chaja-key": options.key
|
|
98
|
+
},
|
|
99
|
+
body,
|
|
100
|
+
signal: AbortSignal.timeout(1e4)
|
|
101
|
+
});
|
|
102
|
+
const retry = parseRetryAfter(response.headers.get("retry-after"));
|
|
103
|
+
return {
|
|
104
|
+
status: response.status,
|
|
105
|
+
...retry === void 0 ? {} : { retryAfterSeconds: retry }
|
|
106
|
+
};
|
|
107
|
+
} catch {
|
|
108
|
+
return { status: 0 };
|
|
109
|
+
}
|
|
110
|
+
} };
|
|
111
|
+
const client = createClient(options, {
|
|
112
|
+
sdk: {
|
|
113
|
+
name: "chaja.javascript.node",
|
|
114
|
+
version: SDK_VERSION
|
|
115
|
+
},
|
|
116
|
+
platform: "node",
|
|
117
|
+
transport,
|
|
118
|
+
eventId() {
|
|
119
|
+
return randomUUID().replaceAll("-", "");
|
|
120
|
+
},
|
|
121
|
+
now: Date.now,
|
|
122
|
+
random: Math.random,
|
|
123
|
+
schedule(callback, delayMs) {
|
|
124
|
+
const handle = setTimeout(callback, delayMs);
|
|
125
|
+
handle.unref();
|
|
126
|
+
return handle;
|
|
127
|
+
},
|
|
128
|
+
cancel(handle) {
|
|
129
|
+
if (isTimer(handle)) clearTimeout(handle);
|
|
130
|
+
},
|
|
131
|
+
enrich(item) {
|
|
132
|
+
return {
|
|
133
|
+
...item,
|
|
134
|
+
...item.exceptions ? { exceptions: item.exceptions.map(function exception(entry) {
|
|
135
|
+
return {
|
|
136
|
+
...entry,
|
|
137
|
+
frames: entry.frames.map(function frame(value) {
|
|
138
|
+
return withContext({
|
|
139
|
+
...value,
|
|
140
|
+
inApp: value.inApp ?? inApp(value)
|
|
141
|
+
});
|
|
142
|
+
})
|
|
143
|
+
};
|
|
144
|
+
}) } : {},
|
|
145
|
+
attributes: {
|
|
146
|
+
"process.runtime.version": process.version,
|
|
147
|
+
"host.arch": process.arch,
|
|
148
|
+
"os.type": process.platform,
|
|
149
|
+
...item.attributes
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
},
|
|
153
|
+
...options.offlineDirectory ? { onUndeliverable: persist(options.offlineDirectory) } : {}
|
|
154
|
+
});
|
|
155
|
+
if (options.processHandlers !== false) {
|
|
156
|
+
/**
|
|
157
|
+
* Report an uncaught exception, flush and exit like Node would.
|
|
158
|
+
*
|
|
159
|
+
* @param error - Uncaught error.
|
|
160
|
+
*/
|
|
161
|
+
function onUncaught(error) {
|
|
162
|
+
client.captureException(error, {
|
|
163
|
+
level: "fatal",
|
|
164
|
+
mechanism: {
|
|
165
|
+
type: "uncaughtException",
|
|
166
|
+
handled: false
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
if (options.exitOnUncaught !== false) client.flush(2e3).finally(function exit() {
|
|
170
|
+
process.exit(1);
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Report an unhandled promise rejection.
|
|
175
|
+
*
|
|
176
|
+
* @param reason - Rejection reason.
|
|
177
|
+
*/
|
|
178
|
+
function onRejection(reason) {
|
|
179
|
+
client.captureException(reason, { mechanism: {
|
|
180
|
+
type: "unhandledRejection",
|
|
181
|
+
handled: false
|
|
182
|
+
} });
|
|
183
|
+
}
|
|
184
|
+
process.on("uncaughtException", onUncaught);
|
|
185
|
+
process.on("unhandledRejection", onRejection);
|
|
186
|
+
cleanups.push(function remove() {
|
|
187
|
+
process.off("uncaughtException", onUncaught);
|
|
188
|
+
process.off("unhandledRejection", onRejection);
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
if (options.offlineDirectory) replay(options.offlineDirectory, transport);
|
|
192
|
+
const instance = {
|
|
193
|
+
...client,
|
|
194
|
+
fetch: send,
|
|
195
|
+
async close(timeoutMs = 2e3) {
|
|
196
|
+
for (const cleanup of cleanups.splice(0)) cleanup();
|
|
197
|
+
if (current === instance) current = void 0;
|
|
198
|
+
return client.flush(timeoutMs);
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
current = instance;
|
|
202
|
+
return instance;
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Persist undeliverable envelopes as files.
|
|
206
|
+
*
|
|
207
|
+
* @param directory - Offline directory.
|
|
208
|
+
* @returns Callback for the queue.
|
|
209
|
+
*/
|
|
210
|
+
function persist(directory) {
|
|
211
|
+
return function save(body) {
|
|
212
|
+
try {
|
|
213
|
+
mkdirSync(directory, { recursive: true });
|
|
214
|
+
writeFileSync(join(directory, `${Date.now()}-${randomUUID()}.json`), body);
|
|
215
|
+
} catch {}
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Resend envelopes saved by a previous run, deleting the ones the server accepted or rejected for good.
|
|
220
|
+
*
|
|
221
|
+
* @param directory - Offline directory.
|
|
222
|
+
* @param transport - Transport.
|
|
223
|
+
*/
|
|
224
|
+
function replay(directory, transport) {
|
|
225
|
+
if (!existsSync(directory)) return;
|
|
226
|
+
(async function resend() {
|
|
227
|
+
for (const name of readdirSync(directory).filter(isEnvelopeFile).toSorted()) {
|
|
228
|
+
const path = join(directory, name);
|
|
229
|
+
const result = await transport.send(readFileSync(path, "utf8"));
|
|
230
|
+
if (result.status === 0 || result.status === 429 || result.status >= 500) return;
|
|
231
|
+
rmSync(path, { force: true });
|
|
232
|
+
}
|
|
233
|
+
})();
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Whether a file name looks like a saved envelope.
|
|
237
|
+
*
|
|
238
|
+
* @param name - File name.
|
|
239
|
+
* @returns True for `.json` files.
|
|
240
|
+
*/
|
|
241
|
+
function isEnvelopeFile(name) {
|
|
242
|
+
return name.endsWith(".json");
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Request details from a Node-style request object, without the body.
|
|
246
|
+
*
|
|
247
|
+
* @param request - Incoming request.
|
|
248
|
+
* @returns Request info.
|
|
249
|
+
*/
|
|
250
|
+
function requestInfo(request) {
|
|
251
|
+
const headers = {};
|
|
252
|
+
for (const [key, value] of Object.entries(request.headers ?? {})) if (value !== void 0) headers[key] = Array.isArray(value) ? value.join(", ") : value;
|
|
253
|
+
return {
|
|
254
|
+
...request.method ? { method: request.method } : {},
|
|
255
|
+
...request.originalUrl ?? request.url ? { url: request.originalUrl ?? request.url } : {},
|
|
256
|
+
headers
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* Express error middleware: reports the error with request details, then passes it on.
|
|
261
|
+
*
|
|
262
|
+
* @param client - Client; defaults to the one from `init`.
|
|
263
|
+
* @returns Express error handler.
|
|
264
|
+
*/
|
|
265
|
+
function expressErrorHandler(client) {
|
|
266
|
+
return function chajaErrorHandler(error, request, _response, next) {
|
|
267
|
+
(client ?? current)?.captureException(error, {
|
|
268
|
+
request: requestInfo(request),
|
|
269
|
+
mechanism: {
|
|
270
|
+
type: "express",
|
|
271
|
+
handled: false
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
next(error);
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Report an error from a Fetch-API style request (Hono, Next.js route handlers, Bun, Deno).
|
|
279
|
+
*
|
|
280
|
+
* @param error - Thrown value.
|
|
281
|
+
* @param request - Web request.
|
|
282
|
+
* @param context - Extra context.
|
|
283
|
+
* @returns Event id, if sent.
|
|
284
|
+
*/
|
|
285
|
+
function captureRequestError(error, request, context = {}) {
|
|
286
|
+
const headers = {};
|
|
287
|
+
request.headers.forEach(function copy(value, key) {
|
|
288
|
+
headers[key] = value;
|
|
289
|
+
});
|
|
290
|
+
return current?.captureException(error, {
|
|
291
|
+
...context,
|
|
292
|
+
request: {
|
|
293
|
+
method: request.method,
|
|
294
|
+
url: request.url,
|
|
295
|
+
headers
|
|
296
|
+
},
|
|
297
|
+
mechanism: context.mechanism ?? {
|
|
298
|
+
type: "request",
|
|
299
|
+
handled: false
|
|
300
|
+
}
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* Capture an error with the client from `init`.
|
|
305
|
+
*
|
|
306
|
+
* @param error - Thrown value.
|
|
307
|
+
* @param context - Extra context.
|
|
308
|
+
* @returns Event id, if sent.
|
|
309
|
+
*/
|
|
310
|
+
function captureException(error, context) {
|
|
311
|
+
return current?.captureException(error, context);
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* Capture a message with the client from `init`.
|
|
315
|
+
*
|
|
316
|
+
* @param message - Message.
|
|
317
|
+
* @param level - Level.
|
|
318
|
+
* @param context - Extra context.
|
|
319
|
+
* @returns Event id, if sent.
|
|
320
|
+
*/
|
|
321
|
+
function captureMessage(message, level, context) {
|
|
322
|
+
return current?.captureMessage(message, level, context);
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* Record a product analytics event for a person with the client from `init`. Servers act for many people, so every event
|
|
326
|
+
* names its person.
|
|
327
|
+
*
|
|
328
|
+
* @param event - Person, name and properties.
|
|
329
|
+
* @returns Event id, if sent.
|
|
330
|
+
* @example
|
|
331
|
+
* capture({ distinctId: user.id, name: "subscription_renewed", properties: { plan: "pro" } });
|
|
332
|
+
*/
|
|
333
|
+
function capture(event) {
|
|
334
|
+
return current?.capture(event.name, event.properties, {
|
|
335
|
+
distinctId: event.distinctId,
|
|
336
|
+
...event.set ? { set: event.set } : {},
|
|
337
|
+
...event.setOnce ? { setOnce: event.setOnce } : {}
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* Send queued events.
|
|
342
|
+
*
|
|
343
|
+
* @param timeoutMs - Maximum wait.
|
|
344
|
+
* @returns Whether everything was sent.
|
|
345
|
+
*/
|
|
346
|
+
async function flush(timeoutMs) {
|
|
347
|
+
return current ? current.flush(timeoutMs) : true;
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* The client created by `init`, if any.
|
|
351
|
+
*
|
|
352
|
+
* @returns Current client.
|
|
353
|
+
*/
|
|
354
|
+
function getClient() {
|
|
355
|
+
return current;
|
|
356
|
+
}
|
|
357
|
+
const FLAG_CACHE_MS = 3e4;
|
|
358
|
+
const flagCache = /* @__PURE__ */ new Map();
|
|
359
|
+
/**
|
|
360
|
+
* Feature flags for a person, with the client from `init`, cached for 30 seconds per person and properties.
|
|
361
|
+
*
|
|
362
|
+
* @param distinctId - Your id for the person.
|
|
363
|
+
* @param properties - Properties the flags' conditions may test, on top of what product analytics knows about them.
|
|
364
|
+
* @returns Flag values: `false`, `true` or a variant key; empty when the server cannot be reached.
|
|
365
|
+
* @example
|
|
366
|
+
* const flags = await getFeatureFlags(user.id, { plan: user.plan });
|
|
367
|
+
* if (flags["new-checkout"]) { ... }
|
|
368
|
+
*/
|
|
369
|
+
async function getFeatureFlags(distinctId, properties = {}) {
|
|
370
|
+
const client = current;
|
|
371
|
+
if (!client) return {};
|
|
372
|
+
const cacheKey = JSON.stringify([distinctId, properties]);
|
|
373
|
+
const cached = flagCache.get(cacheKey);
|
|
374
|
+
if (cached && Date.now() - cached.at < FLAG_CACHE_MS) return cached.flags;
|
|
375
|
+
try {
|
|
376
|
+
const response = await client.fetch(`${client.options.endpoint.replace(/\/+$/u, "")}/api/v1/flags`, {
|
|
377
|
+
method: "POST",
|
|
378
|
+
headers: {
|
|
379
|
+
"content-type": "application/json",
|
|
380
|
+
"x-chaja-key": client.options.key
|
|
381
|
+
},
|
|
382
|
+
body: JSON.stringify({
|
|
383
|
+
distinctId,
|
|
384
|
+
properties
|
|
385
|
+
}),
|
|
386
|
+
signal: AbortSignal.timeout(5e3)
|
|
387
|
+
});
|
|
388
|
+
const body = response.ok ? await response.json() : void 0;
|
|
389
|
+
const raw = typeof body === "object" && body !== null && "flags" in body && typeof body.flags === "object" && body.flags !== null ? body.flags : {};
|
|
390
|
+
const flags = {};
|
|
391
|
+
for (const [key, value] of Object.entries(raw)) if (typeof value === "boolean" || typeof value === "string") flags[key] = value;
|
|
392
|
+
flagCache.set(cacheKey, {
|
|
393
|
+
at: Date.now(),
|
|
394
|
+
flags
|
|
395
|
+
});
|
|
396
|
+
return flags;
|
|
397
|
+
} catch {
|
|
398
|
+
return cached?.flags ?? {};
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
/**
|
|
402
|
+
* Whether a flag is on for a person (a variant counts as on), with the client from `init`.
|
|
403
|
+
*
|
|
404
|
+
* @param key - Flag key.
|
|
405
|
+
* @param distinctId - Your id for the person.
|
|
406
|
+
* @param properties - Properties for the flag's conditions.
|
|
407
|
+
* @returns True when on.
|
|
408
|
+
*/
|
|
409
|
+
async function isFeatureEnabled(key, distinctId, properties = {}) {
|
|
410
|
+
const value = (await getFeatureFlags(distinctId, properties))[key];
|
|
411
|
+
return value !== void 0 && value !== false;
|
|
412
|
+
}
|
|
413
|
+
//#endregion
|
|
414
|
+
export { capture, captureException, captureMessage, captureRequestError, expressErrorHandler, flush, getClient, getFeatureFlags, init, isFeatureEnabled, requestInfo };
|
package/package.json
CHANGED
|
@@ -1,6 +1,50 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chaja/sdk-node",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Chajá error tracking for Node.js: process handlers, source context, offline buffer, Express and Fetch helpers.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"chaja",
|
|
7
|
+
"error-tracking",
|
|
8
|
+
"observability",
|
|
9
|
+
"node",
|
|
10
|
+
"sdk"
|
|
11
|
+
],
|
|
12
|
+
"homepage": "https://github.com/emiliodominguez/chaja#readme",
|
|
13
|
+
"bugs": "https://github.com/emiliodominguez/chaja/issues",
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "git+https://github.com/emiliodominguez/chaja.git",
|
|
17
|
+
"directory": "packages/sdk-node"
|
|
18
|
+
},
|
|
19
|
+
"type": "module",
|
|
20
|
+
"nx": {
|
|
21
|
+
"tags": [
|
|
22
|
+
"scope:sdk",
|
|
23
|
+
"type:sdk"
|
|
24
|
+
]
|
|
25
|
+
},
|
|
26
|
+
"exports": {
|
|
27
|
+
".": {
|
|
28
|
+
"types": "./dist/index.d.mts",
|
|
29
|
+
"default": "./dist/index.mjs"
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"files": [
|
|
33
|
+
"dist"
|
|
34
|
+
],
|
|
35
|
+
"sideEffects": false,
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@chaja/protocol": "0.0.0",
|
|
38
|
+
"tsdown": "0.23.0"
|
|
39
|
+
},
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"@chaja/sdk-core": "0.1.0"
|
|
42
|
+
},
|
|
43
|
+
"engines": {
|
|
44
|
+
"node": ">=22"
|
|
45
|
+
},
|
|
46
|
+
"scripts": {
|
|
47
|
+
"build": "tsdown src/index.ts --format esm --dts --clean --out-dir dist",
|
|
48
|
+
"typecheck": "tsc --noEmit -p ."
|
|
49
|
+
}
|
|
6
50
|
}
|