@pionne/node 0.2.0 → 0.3.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/dist/index.cjs +109 -14
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.mts +10 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.mjs +109 -14
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
- package/src/index.ts +44 -2
- package/src/sessions.ts +108 -0
package/dist/index.cjs
CHANGED
|
@@ -35,7 +35,78 @@ __export(index_exports, {
|
|
|
35
35
|
});
|
|
36
36
|
module.exports = __toCommonJS(index_exports);
|
|
37
37
|
var os = __toESM(require("os"));
|
|
38
|
-
var
|
|
38
|
+
var process2 = __toESM(require("process"));
|
|
39
|
+
|
|
40
|
+
// src/sessions.ts
|
|
41
|
+
var crypto = __toESM(require("crypto"));
|
|
42
|
+
var current = null;
|
|
43
|
+
function sessionsUrl(ingestEndpoint) {
|
|
44
|
+
if (ingestEndpoint.endsWith("/ingest")) {
|
|
45
|
+
return ingestEndpoint.slice(0, -"/ingest".length) + "/sessions";
|
|
46
|
+
}
|
|
47
|
+
return ingestEndpoint.replace(/\/+$/, "") + "/sessions";
|
|
48
|
+
}
|
|
49
|
+
function postSession(state, status, durationMs) {
|
|
50
|
+
const url = sessionsUrl(state.ctx.endpoint);
|
|
51
|
+
const body = {
|
|
52
|
+
session_id: state.id,
|
|
53
|
+
status,
|
|
54
|
+
release: state.ctx.release,
|
|
55
|
+
environment: state.ctx.environment,
|
|
56
|
+
app_version: state.ctx.appVersion,
|
|
57
|
+
os_name: state.ctx.osName,
|
|
58
|
+
user_id_anon: state.ctx.userIdAnon,
|
|
59
|
+
duration_ms: durationMs
|
|
60
|
+
};
|
|
61
|
+
for (const k of Object.keys(body)) {
|
|
62
|
+
if (body[k] === void 0) delete body[k];
|
|
63
|
+
}
|
|
64
|
+
fetch(url, {
|
|
65
|
+
method: "POST",
|
|
66
|
+
headers: {
|
|
67
|
+
"Content-Type": "application/json",
|
|
68
|
+
"X-Pionne-Token": state.ctx.token
|
|
69
|
+
},
|
|
70
|
+
body: JSON.stringify(body)
|
|
71
|
+
}).catch(() => void 0);
|
|
72
|
+
}
|
|
73
|
+
function startSession(ctx) {
|
|
74
|
+
current = {
|
|
75
|
+
id: crypto.randomUUID(),
|
|
76
|
+
startedAt: Date.now(),
|
|
77
|
+
status: "ok",
|
|
78
|
+
ctx
|
|
79
|
+
};
|
|
80
|
+
postSession(current, "ok");
|
|
81
|
+
const onShutdown = () => {
|
|
82
|
+
if (!current || current.status !== "ok") return;
|
|
83
|
+
flipSession("exited");
|
|
84
|
+
};
|
|
85
|
+
process.once("beforeExit", onShutdown);
|
|
86
|
+
return current.id;
|
|
87
|
+
}
|
|
88
|
+
function flipSession(status) {
|
|
89
|
+
if (!current) return;
|
|
90
|
+
const rank = { ok: 0, exited: 1, errored: 2, abnormal: 3, crashed: 4 };
|
|
91
|
+
if (rank[status] <= rank[current.status]) return;
|
|
92
|
+
current.status = status;
|
|
93
|
+
postSession(current, status, Date.now() - current.startedAt);
|
|
94
|
+
}
|
|
95
|
+
function endSession(status = "exited") {
|
|
96
|
+
if (!current) return;
|
|
97
|
+
flipSession(status);
|
|
98
|
+
current = null;
|
|
99
|
+
}
|
|
100
|
+
function getCurrentSessionId() {
|
|
101
|
+
return current?.id ?? null;
|
|
102
|
+
}
|
|
103
|
+
function flipFromEvent(level, mechanismType) {
|
|
104
|
+
if (mechanismType === "manual") return;
|
|
105
|
+
if (level === "fatal") flipSession("crashed");
|
|
106
|
+
else if (level === "error") flipSession("errored");
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// src/index.ts
|
|
39
110
|
var DEFAULT_ENDPOINT = "https://pionne.agkgcreations.fr/api/ingest";
|
|
40
111
|
var DEFAULT_MAX_STACK = 50;
|
|
41
112
|
var SDK_NAME = "pionne.node";
|
|
@@ -58,22 +129,22 @@ function gatherStaticContext() {
|
|
|
58
129
|
sdk: { name: SDK_NAME, version: SDK_VERSION },
|
|
59
130
|
runtime: {
|
|
60
131
|
name: "node",
|
|
61
|
-
version:
|
|
62
|
-
v8:
|
|
132
|
+
version: process2.versions.node,
|
|
133
|
+
v8: process2.versions.v8
|
|
63
134
|
},
|
|
64
135
|
os: {
|
|
65
136
|
name: os.type(),
|
|
66
137
|
version: os.release(),
|
|
67
|
-
platform:
|
|
68
|
-
arch:
|
|
138
|
+
platform: process2.platform,
|
|
139
|
+
arch: process2.arch,
|
|
69
140
|
cpu_count: os.cpus().length,
|
|
70
141
|
total_memory: os.totalmem(),
|
|
71
142
|
free_memory: os.freemem()
|
|
72
143
|
},
|
|
73
144
|
app: {
|
|
74
145
|
hostname: os.hostname(),
|
|
75
|
-
pid:
|
|
76
|
-
cwd:
|
|
146
|
+
pid: process2.pid,
|
|
147
|
+
cwd: process2.cwd()
|
|
77
148
|
}
|
|
78
149
|
}
|
|
79
150
|
};
|
|
@@ -125,22 +196,26 @@ function installUncaughtHandler() {
|
|
|
125
196
|
const event = buildEvent(err, "fatal", "uncaughtException", false);
|
|
126
197
|
if (event) {
|
|
127
198
|
void send(event);
|
|
199
|
+
flipFromEvent(event.level, event.mechanism?.type ?? "uncaughtException");
|
|
128
200
|
}
|
|
129
201
|
};
|
|
130
|
-
|
|
202
|
+
process2.on("uncaughtException", onUncaught);
|
|
131
203
|
}
|
|
132
204
|
function installRejectionHandler() {
|
|
133
205
|
onRejection = (reason) => {
|
|
134
206
|
const err = reason instanceof Error ? reason : new Error(String(reason));
|
|
135
207
|
const event = buildEvent(err, "error", "unhandledRejection", false);
|
|
136
|
-
if (event)
|
|
208
|
+
if (event) {
|
|
209
|
+
void send(event);
|
|
210
|
+
flipFromEvent(event.level, event.mechanism?.type ?? "unhandledRejection");
|
|
211
|
+
}
|
|
137
212
|
};
|
|
138
|
-
|
|
213
|
+
process2.on("unhandledRejection", onRejection);
|
|
139
214
|
}
|
|
140
215
|
var Pionne = {
|
|
141
216
|
init(options) {
|
|
142
217
|
if (!options?.token || !options.token.startsWith("pio_live_")) {
|
|
143
|
-
if (
|
|
218
|
+
if (process2.env.NODE_ENV !== "production") {
|
|
144
219
|
console.warn(
|
|
145
220
|
"[Pionne] Missing or invalid token (must start with pio_live_)."
|
|
146
221
|
);
|
|
@@ -153,7 +228,7 @@ var Pionne = {
|
|
|
153
228
|
token: options.token,
|
|
154
229
|
endpoint: options.endpoint ?? DEFAULT_ENDPOINT,
|
|
155
230
|
release: options.release,
|
|
156
|
-
environment: options.environment ??
|
|
231
|
+
environment: options.environment ?? process2.env.NODE_ENV ?? "production",
|
|
157
232
|
enabled: options.enabled ?? true,
|
|
158
233
|
captureUncaughtExceptions: options.captureUncaughtExceptions ?? true,
|
|
159
234
|
captureUnhandledRejections: options.captureUnhandledRejections ?? true,
|
|
@@ -165,6 +240,17 @@ var Pionne = {
|
|
|
165
240
|
};
|
|
166
241
|
if (config.captureUncaughtExceptions) installUncaughtHandler();
|
|
167
242
|
if (config.captureUnhandledRejections) installRejectionHandler();
|
|
243
|
+
if (options.releaseHealth !== false) {
|
|
244
|
+
startSession({
|
|
245
|
+
endpoint: config.endpoint,
|
|
246
|
+
token: config.token,
|
|
247
|
+
release: config.release,
|
|
248
|
+
environment: config.environment,
|
|
249
|
+
appVersion: staticContext.app_version,
|
|
250
|
+
osName: staticContext.os_name,
|
|
251
|
+
userIdAnon: config.userIdAnon
|
|
252
|
+
});
|
|
253
|
+
}
|
|
168
254
|
},
|
|
169
255
|
captureException(err, extra) {
|
|
170
256
|
const event = buildEvent(
|
|
@@ -203,12 +289,21 @@ var Pionne = {
|
|
|
203
289
|
* clean shutdown. Re-init by calling `init()` again.
|
|
204
290
|
*/
|
|
205
291
|
uninstall() {
|
|
206
|
-
if (onUncaught)
|
|
207
|
-
if (onRejection)
|
|
292
|
+
if (onUncaught) process2.removeListener("uncaughtException", onUncaught);
|
|
293
|
+
if (onRejection) process2.removeListener("unhandledRejection", onRejection);
|
|
208
294
|
onUncaught = null;
|
|
209
295
|
onRejection = null;
|
|
210
296
|
config = null;
|
|
211
297
|
staticContext = {};
|
|
298
|
+
},
|
|
299
|
+
// ─── Release Health ───────────────────────────────────────────────────
|
|
300
|
+
/** Manually end the current session (status='exited'). */
|
|
301
|
+
endSession() {
|
|
302
|
+
endSession();
|
|
303
|
+
},
|
|
304
|
+
/** UUID of the current open session (for diagnostics). */
|
|
305
|
+
getSessionId() {
|
|
306
|
+
return getCurrentSessionId();
|
|
212
307
|
}
|
|
213
308
|
};
|
|
214
309
|
function expressErrorHandler(err, _req, _res, next) {
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["import * as os from 'node:os';\nimport * as process from 'node:process';\n\nexport type Level = 'fatal' | 'error' | 'warning' | 'info';\nexport type MechanismType =\n | 'uncaughtException'\n | 'unhandledRejection'\n | 'manual';\n\nexport interface Mechanism {\n type: MechanismType;\n handled: boolean;\n}\n\nexport interface PionneEvent {\n exception_type: string;\n message?: string | null;\n stack?: string[];\n level?: Level;\n\n release?: string;\n environment?: string;\n app_version?: string;\n os_name?: string;\n os_version?: string;\n user_id_anon?: string;\n locale?: string;\n timezone?: string;\n\n contexts?: Record<string, Record<string, unknown> | undefined>;\n mechanism?: Mechanism;\n tags?: Record<string, string>;\n}\n\nexport interface PionneOptions {\n /** Project token (starts with `pio_live_`). Required. */\n token: string;\n endpoint?: string;\n release?: string;\n environment?: string;\n enabled?: boolean;\n captureUncaughtExceptions?: boolean;\n captureUnhandledRejections?: boolean;\n autoContext?: boolean;\n beforeSend?: (event: PionneEvent) => PionneEvent | null;\n userIdAnon?: string;\n tags?: Record<string, string>;\n maxStackFrames?: number;\n}\n\nconst DEFAULT_ENDPOINT = 'https://pionne.agkgcreations.fr/api/ingest';\nconst DEFAULT_MAX_STACK = 50;\nconst SDK_NAME = 'pionne.node';\nconst SDK_VERSION = '0.1.0';\n\ntype ResolvedConfig = Required<\n Omit<PionneOptions, 'beforeSend' | 'userIdAnon' | 'tags' | 'release'>\n> & {\n beforeSend?: PionneOptions['beforeSend'];\n userIdAnon?: string;\n tags?: Record<string, string>;\n release?: string;\n};\n\nlet config: ResolvedConfig | null = null;\nlet staticContext: Partial<PionneEvent> = {};\nlet onUncaught: ((err: Error) => void) | null = null;\nlet onRejection: ((reason: unknown) => void) | null = null;\n\nfunction gatherStaticContext(): Partial<PionneEvent> {\n let timezone: string | undefined;\n try {\n timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;\n } catch {\n // ignore\n }\n return {\n os_name: os.type(),\n os_version: os.release(),\n timezone,\n contexts: {\n sdk: { name: SDK_NAME, version: SDK_VERSION },\n runtime: {\n name: 'node',\n version: process.versions.node,\n v8: process.versions.v8,\n },\n os: {\n name: os.type(),\n version: os.release(),\n platform: process.platform,\n arch: process.arch,\n cpu_count: os.cpus().length,\n total_memory: os.totalmem(),\n free_memory: os.freemem(),\n },\n app: {\n hostname: os.hostname(),\n pid: process.pid,\n cwd: process.cwd(),\n },\n },\n };\n}\n\nfunction parseStack(error: Error, max: number): string[] {\n if (!error.stack) return [];\n return error.stack\n .split('\\n')\n .slice(0, max)\n .map((s) => s.trim())\n .filter(Boolean);\n}\n\nfunction buildEvent(\n err: unknown,\n level: Level,\n mechanism: MechanismType,\n handled: boolean,\n extra?: Partial<PionneEvent>,\n): PionneEvent | null {\n if (!config || !config.enabled) return null;\n const e = err instanceof Error ? err : new Error(String(err));\n const event: PionneEvent = {\n ...staticContext,\n exception_type: e.name || 'Error',\n message: e.message || null,\n stack: parseStack(e, config.maxStackFrames),\n level,\n release: config.release,\n environment: config.environment,\n user_id_anon: config.userIdAnon,\n tags: config.tags,\n mechanism: { type: mechanism, handled },\n ...extra,\n };\n if (config.beforeSend) {\n const result = config.beforeSend(event);\n if (!result) return null;\n return result;\n }\n return event;\n}\n\nasync function send(event: PionneEvent): Promise<void> {\n if (!config) return;\n try {\n // Node >=18 has global `fetch`. We rely on it instead of pulling in\n // node-fetch / undici as a dependency.\n if (typeof fetch !== 'function') return;\n await fetch(config.endpoint, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'X-Pionne-Token': config.token,\n },\n body: JSON.stringify(event),\n });\n } catch {\n // Best-effort: a monitoring SDK must never crash the host process.\n }\n}\n\nfunction installUncaughtHandler(): void {\n onUncaught = (err: Error) => {\n const event = buildEvent(err, 'fatal', 'uncaughtException', false);\n if (event) {\n // Fire-and-forget: process is going to die anyway. Best we can do is\n // try to flush before exit, but Node will tear down imminently.\n void send(event);\n }\n };\n process.on('uncaughtException', onUncaught);\n}\n\nfunction installRejectionHandler(): void {\n onRejection = (reason: unknown) => {\n const err = reason instanceof Error ? reason : new Error(String(reason));\n const event = buildEvent(err, 'error', 'unhandledRejection', false);\n if (event) void send(event);\n };\n process.on('unhandledRejection', onRejection);\n}\n\nexport const Pionne = {\n init(options: PionneOptions): void {\n if (!options?.token || !options.token.startsWith('pio_live_')) {\n if (process.env.NODE_ENV !== 'production') {\n console.warn(\n '[Pionne] Missing or invalid token (must start with pio_live_).',\n );\n }\n return;\n }\n\n const autoContext = options.autoContext ?? true;\n staticContext = autoContext ? gatherStaticContext() : {};\n\n config = {\n token: options.token,\n endpoint: options.endpoint ?? DEFAULT_ENDPOINT,\n release: options.release,\n environment:\n options.environment ?? process.env.NODE_ENV ?? 'production',\n enabled: options.enabled ?? true,\n captureUncaughtExceptions: options.captureUncaughtExceptions ?? true,\n captureUnhandledRejections: options.captureUnhandledRejections ?? true,\n autoContext,\n beforeSend: options.beforeSend,\n userIdAnon: options.userIdAnon,\n tags: options.tags,\n maxStackFrames: options.maxStackFrames ?? DEFAULT_MAX_STACK,\n };\n\n if (config.captureUncaughtExceptions) installUncaughtHandler();\n if (config.captureUnhandledRejections) installRejectionHandler();\n },\n\n captureException(err: unknown, extra?: Partial<PionneEvent>): void {\n const event = buildEvent(\n err,\n extra?.level ?? 'error',\n 'manual',\n true,\n extra,\n );\n if (event) void send(event);\n },\n\n captureMessage(message: string, extra?: Partial<PionneEvent>): void {\n const event = buildEvent(\n new Error(message),\n extra?.level ?? 'info',\n 'manual',\n true,\n { exception_type: 'Message', ...extra },\n );\n if (event) void send(event);\n },\n\n setUser(userIdAnon: string | null): void {\n if (!config) return;\n config.userIdAnon = userIdAnon ?? undefined;\n },\n\n setTags(tags: Record<string, string> | null): void {\n if (!config) return;\n config.tags = tags ?? undefined;\n },\n\n setEnabled(enabled: boolean): void {\n if (!config) return;\n config.enabled = enabled;\n },\n\n /**\n * Detach all auto handlers. Useful in tests / CLI scripts that need a\n * clean shutdown. Re-init by calling `init()` again.\n */\n uninstall(): void {\n if (onUncaught) process.removeListener('uncaughtException', onUncaught);\n if (onRejection) process.removeListener('unhandledRejection', onRejection);\n onUncaught = null;\n onRejection = null;\n config = null;\n staticContext = {};\n },\n};\n\n/**\n * Express / Connect / NestJS error middleware. Reports the error then passes\n * it down the chain. Mount it AFTER your routes:\n *\n * import { Pionne, expressErrorHandler } from '@pionne/node';\n * app.use(expressErrorHandler);\n * // your fallback error handler here\n */\nexport function expressErrorHandler(\n err: unknown,\n _req: unknown,\n _res: unknown,\n next: (err?: unknown) => void,\n): void {\n Pionne.captureException(err);\n next(err);\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/sessions.ts"],"sourcesContent":["import * as os from 'node:os';\nimport * as process from 'node:process';\n\nimport {\n endSession as _endSession,\n flipFromEvent,\n getCurrentSessionId,\n startSession as _startSession,\n} from './sessions';\n\nexport type Level = 'fatal' | 'error' | 'warning' | 'info';\nexport type MechanismType =\n | 'uncaughtException'\n | 'unhandledRejection'\n | 'manual';\n\nexport interface Mechanism {\n type: MechanismType;\n handled: boolean;\n}\n\nexport interface PionneEvent {\n exception_type: string;\n message?: string | null;\n stack?: string[];\n level?: Level;\n\n release?: string;\n environment?: string;\n app_version?: string;\n os_name?: string;\n os_version?: string;\n user_id_anon?: string;\n locale?: string;\n timezone?: string;\n\n contexts?: Record<string, Record<string, unknown> | undefined>;\n mechanism?: Mechanism;\n tags?: Record<string, string>;\n}\n\nexport interface PionneOptions {\n /** Project token (starts with `pio_live_`). Required. */\n token: string;\n endpoint?: string;\n release?: string;\n environment?: string;\n enabled?: boolean;\n captureUncaughtExceptions?: boolean;\n captureUnhandledRejections?: boolean;\n autoContext?: boolean;\n beforeSend?: (event: PionneEvent) => PionneEvent | null;\n userIdAnon?: string;\n tags?: Record<string, string>;\n maxStackFrames?: number;\n /**\n * Release Health — opens a session at init() with status='ok', flips to\n * 'crashed'/'errored' if a fatal/error fires through the global handlers.\n * The dashboard derives crash-free user rate per release. Default: true.\n */\n releaseHealth?: boolean;\n}\n\nconst DEFAULT_ENDPOINT = 'https://pionne.agkgcreations.fr/api/ingest';\nconst DEFAULT_MAX_STACK = 50;\nconst SDK_NAME = 'pionne.node';\nconst SDK_VERSION = '0.1.0';\n\ntype ResolvedConfig = Required<\n Omit<PionneOptions, 'beforeSend' | 'userIdAnon' | 'tags' | 'release' | 'releaseHealth'>\n> & {\n beforeSend?: PionneOptions['beforeSend'];\n userIdAnon?: string;\n tags?: Record<string, string>;\n release?: string;\n};\n\nlet config: ResolvedConfig | null = null;\nlet staticContext: Partial<PionneEvent> = {};\nlet onUncaught: ((err: Error) => void) | null = null;\nlet onRejection: ((reason: unknown) => void) | null = null;\n\nfunction gatherStaticContext(): Partial<PionneEvent> {\n let timezone: string | undefined;\n try {\n timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;\n } catch {\n // ignore\n }\n return {\n os_name: os.type(),\n os_version: os.release(),\n timezone,\n contexts: {\n sdk: { name: SDK_NAME, version: SDK_VERSION },\n runtime: {\n name: 'node',\n version: process.versions.node,\n v8: process.versions.v8,\n },\n os: {\n name: os.type(),\n version: os.release(),\n platform: process.platform,\n arch: process.arch,\n cpu_count: os.cpus().length,\n total_memory: os.totalmem(),\n free_memory: os.freemem(),\n },\n app: {\n hostname: os.hostname(),\n pid: process.pid,\n cwd: process.cwd(),\n },\n },\n };\n}\n\nfunction parseStack(error: Error, max: number): string[] {\n if (!error.stack) return [];\n return error.stack\n .split('\\n')\n .slice(0, max)\n .map((s) => s.trim())\n .filter(Boolean);\n}\n\nfunction buildEvent(\n err: unknown,\n level: Level,\n mechanism: MechanismType,\n handled: boolean,\n extra?: Partial<PionneEvent>,\n): PionneEvent | null {\n if (!config || !config.enabled) return null;\n const e = err instanceof Error ? err : new Error(String(err));\n const event: PionneEvent = {\n ...staticContext,\n exception_type: e.name || 'Error',\n message: e.message || null,\n stack: parseStack(e, config.maxStackFrames),\n level,\n release: config.release,\n environment: config.environment,\n user_id_anon: config.userIdAnon,\n tags: config.tags,\n mechanism: { type: mechanism, handled },\n ...extra,\n };\n if (config.beforeSend) {\n const result = config.beforeSend(event);\n if (!result) return null;\n return result;\n }\n return event;\n}\n\nasync function send(event: PionneEvent): Promise<void> {\n if (!config) return;\n try {\n // Node >=18 has global `fetch`. We rely on it instead of pulling in\n // node-fetch / undici as a dependency.\n if (typeof fetch !== 'function') return;\n await fetch(config.endpoint, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'X-Pionne-Token': config.token,\n },\n body: JSON.stringify(event),\n });\n } catch {\n // Best-effort: a monitoring SDK must never crash the host process.\n }\n}\n\nfunction installUncaughtHandler(): void {\n onUncaught = (err: Error) => {\n const event = buildEvent(err, 'fatal', 'uncaughtException', false);\n if (event) {\n // Fire-and-forget: process is going to die anyway. Best we can do is\n // try to flush before exit, but Node will tear down imminently.\n void send(event);\n flipFromEvent(event.level, event.mechanism?.type ?? 'uncaughtException');\n }\n };\n process.on('uncaughtException', onUncaught);\n}\n\nfunction installRejectionHandler(): void {\n onRejection = (reason: unknown) => {\n const err = reason instanceof Error ? reason : new Error(String(reason));\n const event = buildEvent(err, 'error', 'unhandledRejection', false);\n if (event) {\n void send(event);\n flipFromEvent(event.level, event.mechanism?.type ?? 'unhandledRejection');\n }\n };\n process.on('unhandledRejection', onRejection);\n}\n\nexport const Pionne = {\n init(options: PionneOptions): void {\n if (!options?.token || !options.token.startsWith('pio_live_')) {\n if (process.env.NODE_ENV !== 'production') {\n console.warn(\n '[Pionne] Missing or invalid token (must start with pio_live_).',\n );\n }\n return;\n }\n\n const autoContext = options.autoContext ?? true;\n staticContext = autoContext ? gatherStaticContext() : {};\n\n config = {\n token: options.token,\n endpoint: options.endpoint ?? DEFAULT_ENDPOINT,\n release: options.release,\n environment:\n options.environment ?? process.env.NODE_ENV ?? 'production',\n enabled: options.enabled ?? true,\n captureUncaughtExceptions: options.captureUncaughtExceptions ?? true,\n captureUnhandledRejections: options.captureUnhandledRejections ?? true,\n autoContext,\n beforeSend: options.beforeSend,\n userIdAnon: options.userIdAnon,\n tags: options.tags,\n maxStackFrames: options.maxStackFrames ?? DEFAULT_MAX_STACK,\n };\n\n if (config.captureUncaughtExceptions) installUncaughtHandler();\n if (config.captureUnhandledRejections) installRejectionHandler();\n\n // Release Health — open a session unless the host opted out.\n if (options.releaseHealth !== false) {\n _startSession({\n endpoint: config.endpoint,\n token: config.token,\n release: config.release,\n environment: config.environment,\n appVersion: staticContext.app_version,\n osName: staticContext.os_name,\n userIdAnon: config.userIdAnon,\n });\n }\n },\n\n captureException(err: unknown, extra?: Partial<PionneEvent>): void {\n const event = buildEvent(\n err,\n extra?.level ?? 'error',\n 'manual',\n true,\n extra,\n );\n if (event) void send(event);\n },\n\n captureMessage(message: string, extra?: Partial<PionneEvent>): void {\n const event = buildEvent(\n new Error(message),\n extra?.level ?? 'info',\n 'manual',\n true,\n { exception_type: 'Message', ...extra },\n );\n if (event) void send(event);\n },\n\n setUser(userIdAnon: string | null): void {\n if (!config) return;\n config.userIdAnon = userIdAnon ?? undefined;\n },\n\n setTags(tags: Record<string, string> | null): void {\n if (!config) return;\n config.tags = tags ?? undefined;\n },\n\n setEnabled(enabled: boolean): void {\n if (!config) return;\n config.enabled = enabled;\n },\n\n /**\n * Detach all auto handlers. Useful in tests / CLI scripts that need a\n * clean shutdown. Re-init by calling `init()` again.\n */\n uninstall(): void {\n if (onUncaught) process.removeListener('uncaughtException', onUncaught);\n if (onRejection) process.removeListener('unhandledRejection', onRejection);\n onUncaught = null;\n onRejection = null;\n config = null;\n staticContext = {};\n },\n\n // ─── Release Health ───────────────────────────────────────────────────\n\n /** Manually end the current session (status='exited'). */\n endSession(): void {\n _endSession();\n },\n\n /** UUID of the current open session (for diagnostics). */\n getSessionId(): string | null {\n return getCurrentSessionId();\n },\n};\n\n/**\n * Express / Connect / NestJS error middleware. Reports the error then passes\n * it down the chain. Mount it AFTER your routes:\n *\n * import { Pionne, expressErrorHandler } from '@pionne/node';\n * app.use(expressErrorHandler);\n * // your fallback error handler here\n */\nexport function expressErrorHandler(\n err: unknown,\n _req: unknown,\n _res: unknown,\n next: (err?: unknown) => void,\n): void {\n Pionne.captureException(err);\n next(err);\n}\n","// Release Health for Node.js. Same protocol as the browser/RN SDKs.\n// We auto-flip on uncaughtException/unhandledRejection through the host\n// SDK's existing handlers, and best-effort-emit 'exited' on process.exit.\n\nimport * as crypto from 'node:crypto';\n\nexport type SessionStatus = 'ok' | 'crashed' | 'errored' | 'abnormal' | 'exited';\n\nexport interface SessionContext {\n endpoint: string;\n token: string;\n release?: string;\n environment?: string;\n appVersion?: string;\n osName?: string;\n userIdAnon?: string;\n}\n\ninterface SessionState {\n id: string;\n startedAt: number;\n status: SessionStatus;\n ctx: SessionContext;\n}\n\nlet current: SessionState | null = null;\n\nfunction sessionsUrl(ingestEndpoint: string): string {\n if (ingestEndpoint.endsWith('/ingest')) {\n return ingestEndpoint.slice(0, -'/ingest'.length) + '/sessions';\n }\n return ingestEndpoint.replace(/\\/+$/, '') + '/sessions';\n}\n\nfunction postSession(state: SessionState, status: SessionStatus, durationMs?: number): void {\n const url = sessionsUrl(state.ctx.endpoint);\n const body = {\n session_id: state.id,\n status,\n release: state.ctx.release,\n environment: state.ctx.environment,\n app_version: state.ctx.appVersion,\n os_name: state.ctx.osName,\n user_id_anon: state.ctx.userIdAnon,\n duration_ms: durationMs,\n };\n for (const k of Object.keys(body) as (keyof typeof body)[]) {\n if (body[k] === undefined) delete body[k];\n }\n // Node 18+ has global fetch.\n fetch(url, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'X-Pionne-Token': state.ctx.token,\n },\n body: JSON.stringify(body),\n }).catch(() => undefined);\n}\n\nexport function startSession(ctx: SessionContext): string {\n current = {\n id: crypto.randomUUID(),\n startedAt: Date.now(),\n status: 'ok',\n ctx,\n };\n postSession(current, 'ok');\n\n // Best-effort 'exited' flip on graceful shutdown. We listen on 'exit'\n // (sync only — no async I/O guaranteed) and 'beforeExit' (where async\n // works). The actual session POST is fire-and-forget anyway.\n const onShutdown = () => {\n if (!current || current.status !== 'ok') return;\n flipSession('exited');\n };\n process.once('beforeExit', onShutdown);\n\n return current.id;\n}\n\nexport function flipSession(status: SessionStatus): void {\n if (!current) return;\n const rank: Record<SessionStatus, number> =\n { ok: 0, exited: 1, errored: 2, abnormal: 3, crashed: 4 };\n if (rank[status] <= rank[current.status]) return;\n current.status = status;\n postSession(current, status, Date.now() - current.startedAt);\n}\n\nexport function endSession(status: SessionStatus = 'exited'): void {\n if (!current) return;\n flipSession(status);\n current = null;\n}\n\nexport function getCurrentSessionId(): string | null {\n return current?.id ?? null;\n}\n\nexport function flipFromEvent(\n level: 'fatal' | 'error' | 'warning' | 'info' | undefined,\n mechanismType: string,\n): void {\n if (mechanismType === 'manual') return;\n if (level === 'fatal') flipSession('crashed');\n else if (level === 'error') flipSession('errored');\n}\n"],"mappings":"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","names":["process"]}
|
package/dist/index.d.mts
CHANGED
|
@@ -35,6 +35,12 @@ interface PionneOptions {
|
|
|
35
35
|
userIdAnon?: string;
|
|
36
36
|
tags?: Record<string, string>;
|
|
37
37
|
maxStackFrames?: number;
|
|
38
|
+
/**
|
|
39
|
+
* Release Health — opens a session at init() with status='ok', flips to
|
|
40
|
+
* 'crashed'/'errored' if a fatal/error fires through the global handlers.
|
|
41
|
+
* The dashboard derives crash-free user rate per release. Default: true.
|
|
42
|
+
*/
|
|
43
|
+
releaseHealth?: boolean;
|
|
38
44
|
}
|
|
39
45
|
declare const Pionne: {
|
|
40
46
|
init(options: PionneOptions): void;
|
|
@@ -48,6 +54,10 @@ declare const Pionne: {
|
|
|
48
54
|
* clean shutdown. Re-init by calling `init()` again.
|
|
49
55
|
*/
|
|
50
56
|
uninstall(): void;
|
|
57
|
+
/** Manually end the current session (status='exited'). */
|
|
58
|
+
endSession(): void;
|
|
59
|
+
/** UUID of the current open session (for diagnostics). */
|
|
60
|
+
getSessionId(): string | null;
|
|
51
61
|
};
|
|
52
62
|
/**
|
|
53
63
|
* Express / Connect / NestJS error middleware. Reports the error then passes
|
package/dist/index.d.ts
CHANGED
|
@@ -35,6 +35,12 @@ interface PionneOptions {
|
|
|
35
35
|
userIdAnon?: string;
|
|
36
36
|
tags?: Record<string, string>;
|
|
37
37
|
maxStackFrames?: number;
|
|
38
|
+
/**
|
|
39
|
+
* Release Health — opens a session at init() with status='ok', flips to
|
|
40
|
+
* 'crashed'/'errored' if a fatal/error fires through the global handlers.
|
|
41
|
+
* The dashboard derives crash-free user rate per release. Default: true.
|
|
42
|
+
*/
|
|
43
|
+
releaseHealth?: boolean;
|
|
38
44
|
}
|
|
39
45
|
declare const Pionne: {
|
|
40
46
|
init(options: PionneOptions): void;
|
|
@@ -48,6 +54,10 @@ declare const Pionne: {
|
|
|
48
54
|
* clean shutdown. Re-init by calling `init()` again.
|
|
49
55
|
*/
|
|
50
56
|
uninstall(): void;
|
|
57
|
+
/** Manually end the current session (status='exited'). */
|
|
58
|
+
endSession(): void;
|
|
59
|
+
/** UUID of the current open session (for diagnostics). */
|
|
60
|
+
getSessionId(): string | null;
|
|
51
61
|
};
|
|
52
62
|
/**
|
|
53
63
|
* Express / Connect / NestJS error middleware. Reports the error then passes
|
package/dist/index.mjs
CHANGED
|
@@ -1,6 +1,77 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
2
|
import * as os from "os";
|
|
3
|
-
import * as
|
|
3
|
+
import * as process2 from "process";
|
|
4
|
+
|
|
5
|
+
// src/sessions.ts
|
|
6
|
+
import * as crypto from "crypto";
|
|
7
|
+
var current = null;
|
|
8
|
+
function sessionsUrl(ingestEndpoint) {
|
|
9
|
+
if (ingestEndpoint.endsWith("/ingest")) {
|
|
10
|
+
return ingestEndpoint.slice(0, -"/ingest".length) + "/sessions";
|
|
11
|
+
}
|
|
12
|
+
return ingestEndpoint.replace(/\/+$/, "") + "/sessions";
|
|
13
|
+
}
|
|
14
|
+
function postSession(state, status, durationMs) {
|
|
15
|
+
const url = sessionsUrl(state.ctx.endpoint);
|
|
16
|
+
const body = {
|
|
17
|
+
session_id: state.id,
|
|
18
|
+
status,
|
|
19
|
+
release: state.ctx.release,
|
|
20
|
+
environment: state.ctx.environment,
|
|
21
|
+
app_version: state.ctx.appVersion,
|
|
22
|
+
os_name: state.ctx.osName,
|
|
23
|
+
user_id_anon: state.ctx.userIdAnon,
|
|
24
|
+
duration_ms: durationMs
|
|
25
|
+
};
|
|
26
|
+
for (const k of Object.keys(body)) {
|
|
27
|
+
if (body[k] === void 0) delete body[k];
|
|
28
|
+
}
|
|
29
|
+
fetch(url, {
|
|
30
|
+
method: "POST",
|
|
31
|
+
headers: {
|
|
32
|
+
"Content-Type": "application/json",
|
|
33
|
+
"X-Pionne-Token": state.ctx.token
|
|
34
|
+
},
|
|
35
|
+
body: JSON.stringify(body)
|
|
36
|
+
}).catch(() => void 0);
|
|
37
|
+
}
|
|
38
|
+
function startSession(ctx) {
|
|
39
|
+
current = {
|
|
40
|
+
id: crypto.randomUUID(),
|
|
41
|
+
startedAt: Date.now(),
|
|
42
|
+
status: "ok",
|
|
43
|
+
ctx
|
|
44
|
+
};
|
|
45
|
+
postSession(current, "ok");
|
|
46
|
+
const onShutdown = () => {
|
|
47
|
+
if (!current || current.status !== "ok") return;
|
|
48
|
+
flipSession("exited");
|
|
49
|
+
};
|
|
50
|
+
process.once("beforeExit", onShutdown);
|
|
51
|
+
return current.id;
|
|
52
|
+
}
|
|
53
|
+
function flipSession(status) {
|
|
54
|
+
if (!current) return;
|
|
55
|
+
const rank = { ok: 0, exited: 1, errored: 2, abnormal: 3, crashed: 4 };
|
|
56
|
+
if (rank[status] <= rank[current.status]) return;
|
|
57
|
+
current.status = status;
|
|
58
|
+
postSession(current, status, Date.now() - current.startedAt);
|
|
59
|
+
}
|
|
60
|
+
function endSession(status = "exited") {
|
|
61
|
+
if (!current) return;
|
|
62
|
+
flipSession(status);
|
|
63
|
+
current = null;
|
|
64
|
+
}
|
|
65
|
+
function getCurrentSessionId() {
|
|
66
|
+
return current?.id ?? null;
|
|
67
|
+
}
|
|
68
|
+
function flipFromEvent(level, mechanismType) {
|
|
69
|
+
if (mechanismType === "manual") return;
|
|
70
|
+
if (level === "fatal") flipSession("crashed");
|
|
71
|
+
else if (level === "error") flipSession("errored");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// src/index.ts
|
|
4
75
|
var DEFAULT_ENDPOINT = "https://pionne.agkgcreations.fr/api/ingest";
|
|
5
76
|
var DEFAULT_MAX_STACK = 50;
|
|
6
77
|
var SDK_NAME = "pionne.node";
|
|
@@ -23,22 +94,22 @@ function gatherStaticContext() {
|
|
|
23
94
|
sdk: { name: SDK_NAME, version: SDK_VERSION },
|
|
24
95
|
runtime: {
|
|
25
96
|
name: "node",
|
|
26
|
-
version:
|
|
27
|
-
v8:
|
|
97
|
+
version: process2.versions.node,
|
|
98
|
+
v8: process2.versions.v8
|
|
28
99
|
},
|
|
29
100
|
os: {
|
|
30
101
|
name: os.type(),
|
|
31
102
|
version: os.release(),
|
|
32
|
-
platform:
|
|
33
|
-
arch:
|
|
103
|
+
platform: process2.platform,
|
|
104
|
+
arch: process2.arch,
|
|
34
105
|
cpu_count: os.cpus().length,
|
|
35
106
|
total_memory: os.totalmem(),
|
|
36
107
|
free_memory: os.freemem()
|
|
37
108
|
},
|
|
38
109
|
app: {
|
|
39
110
|
hostname: os.hostname(),
|
|
40
|
-
pid:
|
|
41
|
-
cwd:
|
|
111
|
+
pid: process2.pid,
|
|
112
|
+
cwd: process2.cwd()
|
|
42
113
|
}
|
|
43
114
|
}
|
|
44
115
|
};
|
|
@@ -90,22 +161,26 @@ function installUncaughtHandler() {
|
|
|
90
161
|
const event = buildEvent(err, "fatal", "uncaughtException", false);
|
|
91
162
|
if (event) {
|
|
92
163
|
void send(event);
|
|
164
|
+
flipFromEvent(event.level, event.mechanism?.type ?? "uncaughtException");
|
|
93
165
|
}
|
|
94
166
|
};
|
|
95
|
-
|
|
167
|
+
process2.on("uncaughtException", onUncaught);
|
|
96
168
|
}
|
|
97
169
|
function installRejectionHandler() {
|
|
98
170
|
onRejection = (reason) => {
|
|
99
171
|
const err = reason instanceof Error ? reason : new Error(String(reason));
|
|
100
172
|
const event = buildEvent(err, "error", "unhandledRejection", false);
|
|
101
|
-
if (event)
|
|
173
|
+
if (event) {
|
|
174
|
+
void send(event);
|
|
175
|
+
flipFromEvent(event.level, event.mechanism?.type ?? "unhandledRejection");
|
|
176
|
+
}
|
|
102
177
|
};
|
|
103
|
-
|
|
178
|
+
process2.on("unhandledRejection", onRejection);
|
|
104
179
|
}
|
|
105
180
|
var Pionne = {
|
|
106
181
|
init(options) {
|
|
107
182
|
if (!options?.token || !options.token.startsWith("pio_live_")) {
|
|
108
|
-
if (
|
|
183
|
+
if (process2.env.NODE_ENV !== "production") {
|
|
109
184
|
console.warn(
|
|
110
185
|
"[Pionne] Missing or invalid token (must start with pio_live_)."
|
|
111
186
|
);
|
|
@@ -118,7 +193,7 @@ var Pionne = {
|
|
|
118
193
|
token: options.token,
|
|
119
194
|
endpoint: options.endpoint ?? DEFAULT_ENDPOINT,
|
|
120
195
|
release: options.release,
|
|
121
|
-
environment: options.environment ??
|
|
196
|
+
environment: options.environment ?? process2.env.NODE_ENV ?? "production",
|
|
122
197
|
enabled: options.enabled ?? true,
|
|
123
198
|
captureUncaughtExceptions: options.captureUncaughtExceptions ?? true,
|
|
124
199
|
captureUnhandledRejections: options.captureUnhandledRejections ?? true,
|
|
@@ -130,6 +205,17 @@ var Pionne = {
|
|
|
130
205
|
};
|
|
131
206
|
if (config.captureUncaughtExceptions) installUncaughtHandler();
|
|
132
207
|
if (config.captureUnhandledRejections) installRejectionHandler();
|
|
208
|
+
if (options.releaseHealth !== false) {
|
|
209
|
+
startSession({
|
|
210
|
+
endpoint: config.endpoint,
|
|
211
|
+
token: config.token,
|
|
212
|
+
release: config.release,
|
|
213
|
+
environment: config.environment,
|
|
214
|
+
appVersion: staticContext.app_version,
|
|
215
|
+
osName: staticContext.os_name,
|
|
216
|
+
userIdAnon: config.userIdAnon
|
|
217
|
+
});
|
|
218
|
+
}
|
|
133
219
|
},
|
|
134
220
|
captureException(err, extra) {
|
|
135
221
|
const event = buildEvent(
|
|
@@ -168,12 +254,21 @@ var Pionne = {
|
|
|
168
254
|
* clean shutdown. Re-init by calling `init()` again.
|
|
169
255
|
*/
|
|
170
256
|
uninstall() {
|
|
171
|
-
if (onUncaught)
|
|
172
|
-
if (onRejection)
|
|
257
|
+
if (onUncaught) process2.removeListener("uncaughtException", onUncaught);
|
|
258
|
+
if (onRejection) process2.removeListener("unhandledRejection", onRejection);
|
|
173
259
|
onUncaught = null;
|
|
174
260
|
onRejection = null;
|
|
175
261
|
config = null;
|
|
176
262
|
staticContext = {};
|
|
263
|
+
},
|
|
264
|
+
// ─── Release Health ───────────────────────────────────────────────────
|
|
265
|
+
/** Manually end the current session (status='exited'). */
|
|
266
|
+
endSession() {
|
|
267
|
+
endSession();
|
|
268
|
+
},
|
|
269
|
+
/** UUID of the current open session (for diagnostics). */
|
|
270
|
+
getSessionId() {
|
|
271
|
+
return getCurrentSessionId();
|
|
177
272
|
}
|
|
178
273
|
};
|
|
179
274
|
function expressErrorHandler(err, _req, _res, next) {
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["import * as os from 'node:os';\nimport * as process from 'node:process';\n\nexport type Level = 'fatal' | 'error' | 'warning' | 'info';\nexport type MechanismType =\n | 'uncaughtException'\n | 'unhandledRejection'\n | 'manual';\n\nexport interface Mechanism {\n type: MechanismType;\n handled: boolean;\n}\n\nexport interface PionneEvent {\n exception_type: string;\n message?: string | null;\n stack?: string[];\n level?: Level;\n\n release?: string;\n environment?: string;\n app_version?: string;\n os_name?: string;\n os_version?: string;\n user_id_anon?: string;\n locale?: string;\n timezone?: string;\n\n contexts?: Record<string, Record<string, unknown> | undefined>;\n mechanism?: Mechanism;\n tags?: Record<string, string>;\n}\n\nexport interface PionneOptions {\n /** Project token (starts with `pio_live_`). Required. */\n token: string;\n endpoint?: string;\n release?: string;\n environment?: string;\n enabled?: boolean;\n captureUncaughtExceptions?: boolean;\n captureUnhandledRejections?: boolean;\n autoContext?: boolean;\n beforeSend?: (event: PionneEvent) => PionneEvent | null;\n userIdAnon?: string;\n tags?: Record<string, string>;\n maxStackFrames?: number;\n}\n\nconst DEFAULT_ENDPOINT = 'https://pionne.agkgcreations.fr/api/ingest';\nconst DEFAULT_MAX_STACK = 50;\nconst SDK_NAME = 'pionne.node';\nconst SDK_VERSION = '0.1.0';\n\ntype ResolvedConfig = Required<\n Omit<PionneOptions, 'beforeSend' | 'userIdAnon' | 'tags' | 'release'>\n> & {\n beforeSend?: PionneOptions['beforeSend'];\n userIdAnon?: string;\n tags?: Record<string, string>;\n release?: string;\n};\n\nlet config: ResolvedConfig | null = null;\nlet staticContext: Partial<PionneEvent> = {};\nlet onUncaught: ((err: Error) => void) | null = null;\nlet onRejection: ((reason: unknown) => void) | null = null;\n\nfunction gatherStaticContext(): Partial<PionneEvent> {\n let timezone: string | undefined;\n try {\n timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;\n } catch {\n // ignore\n }\n return {\n os_name: os.type(),\n os_version: os.release(),\n timezone,\n contexts: {\n sdk: { name: SDK_NAME, version: SDK_VERSION },\n runtime: {\n name: 'node',\n version: process.versions.node,\n v8: process.versions.v8,\n },\n os: {\n name: os.type(),\n version: os.release(),\n platform: process.platform,\n arch: process.arch,\n cpu_count: os.cpus().length,\n total_memory: os.totalmem(),\n free_memory: os.freemem(),\n },\n app: {\n hostname: os.hostname(),\n pid: process.pid,\n cwd: process.cwd(),\n },\n },\n };\n}\n\nfunction parseStack(error: Error, max: number): string[] {\n if (!error.stack) return [];\n return error.stack\n .split('\\n')\n .slice(0, max)\n .map((s) => s.trim())\n .filter(Boolean);\n}\n\nfunction buildEvent(\n err: unknown,\n level: Level,\n mechanism: MechanismType,\n handled: boolean,\n extra?: Partial<PionneEvent>,\n): PionneEvent | null {\n if (!config || !config.enabled) return null;\n const e = err instanceof Error ? err : new Error(String(err));\n const event: PionneEvent = {\n ...staticContext,\n exception_type: e.name || 'Error',\n message: e.message || null,\n stack: parseStack(e, config.maxStackFrames),\n level,\n release: config.release,\n environment: config.environment,\n user_id_anon: config.userIdAnon,\n tags: config.tags,\n mechanism: { type: mechanism, handled },\n ...extra,\n };\n if (config.beforeSend) {\n const result = config.beforeSend(event);\n if (!result) return null;\n return result;\n }\n return event;\n}\n\nasync function send(event: PionneEvent): Promise<void> {\n if (!config) return;\n try {\n // Node >=18 has global `fetch`. We rely on it instead of pulling in\n // node-fetch / undici as a dependency.\n if (typeof fetch !== 'function') return;\n await fetch(config.endpoint, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'X-Pionne-Token': config.token,\n },\n body: JSON.stringify(event),\n });\n } catch {\n // Best-effort: a monitoring SDK must never crash the host process.\n }\n}\n\nfunction installUncaughtHandler(): void {\n onUncaught = (err: Error) => {\n const event = buildEvent(err, 'fatal', 'uncaughtException', false);\n if (event) {\n // Fire-and-forget: process is going to die anyway. Best we can do is\n // try to flush before exit, but Node will tear down imminently.\n void send(event);\n }\n };\n process.on('uncaughtException', onUncaught);\n}\n\nfunction installRejectionHandler(): void {\n onRejection = (reason: unknown) => {\n const err = reason instanceof Error ? reason : new Error(String(reason));\n const event = buildEvent(err, 'error', 'unhandledRejection', false);\n if (event) void send(event);\n };\n process.on('unhandledRejection', onRejection);\n}\n\nexport const Pionne = {\n init(options: PionneOptions): void {\n if (!options?.token || !options.token.startsWith('pio_live_')) {\n if (process.env.NODE_ENV !== 'production') {\n console.warn(\n '[Pionne] Missing or invalid token (must start with pio_live_).',\n );\n }\n return;\n }\n\n const autoContext = options.autoContext ?? true;\n staticContext = autoContext ? gatherStaticContext() : {};\n\n config = {\n token: options.token,\n endpoint: options.endpoint ?? DEFAULT_ENDPOINT,\n release: options.release,\n environment:\n options.environment ?? process.env.NODE_ENV ?? 'production',\n enabled: options.enabled ?? true,\n captureUncaughtExceptions: options.captureUncaughtExceptions ?? true,\n captureUnhandledRejections: options.captureUnhandledRejections ?? true,\n autoContext,\n beforeSend: options.beforeSend,\n userIdAnon: options.userIdAnon,\n tags: options.tags,\n maxStackFrames: options.maxStackFrames ?? DEFAULT_MAX_STACK,\n };\n\n if (config.captureUncaughtExceptions) installUncaughtHandler();\n if (config.captureUnhandledRejections) installRejectionHandler();\n },\n\n captureException(err: unknown, extra?: Partial<PionneEvent>): void {\n const event = buildEvent(\n err,\n extra?.level ?? 'error',\n 'manual',\n true,\n extra,\n );\n if (event) void send(event);\n },\n\n captureMessage(message: string, extra?: Partial<PionneEvent>): void {\n const event = buildEvent(\n new Error(message),\n extra?.level ?? 'info',\n 'manual',\n true,\n { exception_type: 'Message', ...extra },\n );\n if (event) void send(event);\n },\n\n setUser(userIdAnon: string | null): void {\n if (!config) return;\n config.userIdAnon = userIdAnon ?? undefined;\n },\n\n setTags(tags: Record<string, string> | null): void {\n if (!config) return;\n config.tags = tags ?? undefined;\n },\n\n setEnabled(enabled: boolean): void {\n if (!config) return;\n config.enabled = enabled;\n },\n\n /**\n * Detach all auto handlers. Useful in tests / CLI scripts that need a\n * clean shutdown. Re-init by calling `init()` again.\n */\n uninstall(): void {\n if (onUncaught) process.removeListener('uncaughtException', onUncaught);\n if (onRejection) process.removeListener('unhandledRejection', onRejection);\n onUncaught = null;\n onRejection = null;\n config = null;\n staticContext = {};\n },\n};\n\n/**\n * Express / Connect / NestJS error middleware. Reports the error then passes\n * it down the chain. Mount it AFTER your routes:\n *\n * import { Pionne, expressErrorHandler } from '@pionne/node';\n * app.use(expressErrorHandler);\n * // your fallback error handler here\n */\nexport function expressErrorHandler(\n err: unknown,\n _req: unknown,\n _res: unknown,\n next: (err?: unknown) => void,\n): void {\n Pionne.captureException(err);\n next(err);\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/sessions.ts"],"sourcesContent":["import * as os from 'node:os';\nimport * as process from 'node:process';\n\nimport {\n endSession as _endSession,\n flipFromEvent,\n getCurrentSessionId,\n startSession as _startSession,\n} from './sessions';\n\nexport type Level = 'fatal' | 'error' | 'warning' | 'info';\nexport type MechanismType =\n | 'uncaughtException'\n | 'unhandledRejection'\n | 'manual';\n\nexport interface Mechanism {\n type: MechanismType;\n handled: boolean;\n}\n\nexport interface PionneEvent {\n exception_type: string;\n message?: string | null;\n stack?: string[];\n level?: Level;\n\n release?: string;\n environment?: string;\n app_version?: string;\n os_name?: string;\n os_version?: string;\n user_id_anon?: string;\n locale?: string;\n timezone?: string;\n\n contexts?: Record<string, Record<string, unknown> | undefined>;\n mechanism?: Mechanism;\n tags?: Record<string, string>;\n}\n\nexport interface PionneOptions {\n /** Project token (starts with `pio_live_`). Required. */\n token: string;\n endpoint?: string;\n release?: string;\n environment?: string;\n enabled?: boolean;\n captureUncaughtExceptions?: boolean;\n captureUnhandledRejections?: boolean;\n autoContext?: boolean;\n beforeSend?: (event: PionneEvent) => PionneEvent | null;\n userIdAnon?: string;\n tags?: Record<string, string>;\n maxStackFrames?: number;\n /**\n * Release Health — opens a session at init() with status='ok', flips to\n * 'crashed'/'errored' if a fatal/error fires through the global handlers.\n * The dashboard derives crash-free user rate per release. Default: true.\n */\n releaseHealth?: boolean;\n}\n\nconst DEFAULT_ENDPOINT = 'https://pionne.agkgcreations.fr/api/ingest';\nconst DEFAULT_MAX_STACK = 50;\nconst SDK_NAME = 'pionne.node';\nconst SDK_VERSION = '0.1.0';\n\ntype ResolvedConfig = Required<\n Omit<PionneOptions, 'beforeSend' | 'userIdAnon' | 'tags' | 'release' | 'releaseHealth'>\n> & {\n beforeSend?: PionneOptions['beforeSend'];\n userIdAnon?: string;\n tags?: Record<string, string>;\n release?: string;\n};\n\nlet config: ResolvedConfig | null = null;\nlet staticContext: Partial<PionneEvent> = {};\nlet onUncaught: ((err: Error) => void) | null = null;\nlet onRejection: ((reason: unknown) => void) | null = null;\n\nfunction gatherStaticContext(): Partial<PionneEvent> {\n let timezone: string | undefined;\n try {\n timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;\n } catch {\n // ignore\n }\n return {\n os_name: os.type(),\n os_version: os.release(),\n timezone,\n contexts: {\n sdk: { name: SDK_NAME, version: SDK_VERSION },\n runtime: {\n name: 'node',\n version: process.versions.node,\n v8: process.versions.v8,\n },\n os: {\n name: os.type(),\n version: os.release(),\n platform: process.platform,\n arch: process.arch,\n cpu_count: os.cpus().length,\n total_memory: os.totalmem(),\n free_memory: os.freemem(),\n },\n app: {\n hostname: os.hostname(),\n pid: process.pid,\n cwd: process.cwd(),\n },\n },\n };\n}\n\nfunction parseStack(error: Error, max: number): string[] {\n if (!error.stack) return [];\n return error.stack\n .split('\\n')\n .slice(0, max)\n .map((s) => s.trim())\n .filter(Boolean);\n}\n\nfunction buildEvent(\n err: unknown,\n level: Level,\n mechanism: MechanismType,\n handled: boolean,\n extra?: Partial<PionneEvent>,\n): PionneEvent | null {\n if (!config || !config.enabled) return null;\n const e = err instanceof Error ? err : new Error(String(err));\n const event: PionneEvent = {\n ...staticContext,\n exception_type: e.name || 'Error',\n message: e.message || null,\n stack: parseStack(e, config.maxStackFrames),\n level,\n release: config.release,\n environment: config.environment,\n user_id_anon: config.userIdAnon,\n tags: config.tags,\n mechanism: { type: mechanism, handled },\n ...extra,\n };\n if (config.beforeSend) {\n const result = config.beforeSend(event);\n if (!result) return null;\n return result;\n }\n return event;\n}\n\nasync function send(event: PionneEvent): Promise<void> {\n if (!config) return;\n try {\n // Node >=18 has global `fetch`. We rely on it instead of pulling in\n // node-fetch / undici as a dependency.\n if (typeof fetch !== 'function') return;\n await fetch(config.endpoint, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'X-Pionne-Token': config.token,\n },\n body: JSON.stringify(event),\n });\n } catch {\n // Best-effort: a monitoring SDK must never crash the host process.\n }\n}\n\nfunction installUncaughtHandler(): void {\n onUncaught = (err: Error) => {\n const event = buildEvent(err, 'fatal', 'uncaughtException', false);\n if (event) {\n // Fire-and-forget: process is going to die anyway. Best we can do is\n // try to flush before exit, but Node will tear down imminently.\n void send(event);\n flipFromEvent(event.level, event.mechanism?.type ?? 'uncaughtException');\n }\n };\n process.on('uncaughtException', onUncaught);\n}\n\nfunction installRejectionHandler(): void {\n onRejection = (reason: unknown) => {\n const err = reason instanceof Error ? reason : new Error(String(reason));\n const event = buildEvent(err, 'error', 'unhandledRejection', false);\n if (event) {\n void send(event);\n flipFromEvent(event.level, event.mechanism?.type ?? 'unhandledRejection');\n }\n };\n process.on('unhandledRejection', onRejection);\n}\n\nexport const Pionne = {\n init(options: PionneOptions): void {\n if (!options?.token || !options.token.startsWith('pio_live_')) {\n if (process.env.NODE_ENV !== 'production') {\n console.warn(\n '[Pionne] Missing or invalid token (must start with pio_live_).',\n );\n }\n return;\n }\n\n const autoContext = options.autoContext ?? true;\n staticContext = autoContext ? gatherStaticContext() : {};\n\n config = {\n token: options.token,\n endpoint: options.endpoint ?? DEFAULT_ENDPOINT,\n release: options.release,\n environment:\n options.environment ?? process.env.NODE_ENV ?? 'production',\n enabled: options.enabled ?? true,\n captureUncaughtExceptions: options.captureUncaughtExceptions ?? true,\n captureUnhandledRejections: options.captureUnhandledRejections ?? true,\n autoContext,\n beforeSend: options.beforeSend,\n userIdAnon: options.userIdAnon,\n tags: options.tags,\n maxStackFrames: options.maxStackFrames ?? DEFAULT_MAX_STACK,\n };\n\n if (config.captureUncaughtExceptions) installUncaughtHandler();\n if (config.captureUnhandledRejections) installRejectionHandler();\n\n // Release Health — open a session unless the host opted out.\n if (options.releaseHealth !== false) {\n _startSession({\n endpoint: config.endpoint,\n token: config.token,\n release: config.release,\n environment: config.environment,\n appVersion: staticContext.app_version,\n osName: staticContext.os_name,\n userIdAnon: config.userIdAnon,\n });\n }\n },\n\n captureException(err: unknown, extra?: Partial<PionneEvent>): void {\n const event = buildEvent(\n err,\n extra?.level ?? 'error',\n 'manual',\n true,\n extra,\n );\n if (event) void send(event);\n },\n\n captureMessage(message: string, extra?: Partial<PionneEvent>): void {\n const event = buildEvent(\n new Error(message),\n extra?.level ?? 'info',\n 'manual',\n true,\n { exception_type: 'Message', ...extra },\n );\n if (event) void send(event);\n },\n\n setUser(userIdAnon: string | null): void {\n if (!config) return;\n config.userIdAnon = userIdAnon ?? undefined;\n },\n\n setTags(tags: Record<string, string> | null): void {\n if (!config) return;\n config.tags = tags ?? undefined;\n },\n\n setEnabled(enabled: boolean): void {\n if (!config) return;\n config.enabled = enabled;\n },\n\n /**\n * Detach all auto handlers. Useful in tests / CLI scripts that need a\n * clean shutdown. Re-init by calling `init()` again.\n */\n uninstall(): void {\n if (onUncaught) process.removeListener('uncaughtException', onUncaught);\n if (onRejection) process.removeListener('unhandledRejection', onRejection);\n onUncaught = null;\n onRejection = null;\n config = null;\n staticContext = {};\n },\n\n // ─── Release Health ───────────────────────────────────────────────────\n\n /** Manually end the current session (status='exited'). */\n endSession(): void {\n _endSession();\n },\n\n /** UUID of the current open session (for diagnostics). */\n getSessionId(): string | null {\n return getCurrentSessionId();\n },\n};\n\n/**\n * Express / Connect / NestJS error middleware. Reports the error then passes\n * it down the chain. Mount it AFTER your routes:\n *\n * import { Pionne, expressErrorHandler } from '@pionne/node';\n * app.use(expressErrorHandler);\n * // your fallback error handler here\n */\nexport function expressErrorHandler(\n err: unknown,\n _req: unknown,\n _res: unknown,\n next: (err?: unknown) => void,\n): void {\n Pionne.captureException(err);\n next(err);\n}\n","// Release Health for Node.js. Same protocol as the browser/RN SDKs.\n// We auto-flip on uncaughtException/unhandledRejection through the host\n// SDK's existing handlers, and best-effort-emit 'exited' on process.exit.\n\nimport * as crypto from 'node:crypto';\n\nexport type SessionStatus = 'ok' | 'crashed' | 'errored' | 'abnormal' | 'exited';\n\nexport interface SessionContext {\n endpoint: string;\n token: string;\n release?: string;\n environment?: string;\n appVersion?: string;\n osName?: string;\n userIdAnon?: string;\n}\n\ninterface SessionState {\n id: string;\n startedAt: number;\n status: SessionStatus;\n ctx: SessionContext;\n}\n\nlet current: SessionState | null = null;\n\nfunction sessionsUrl(ingestEndpoint: string): string {\n if (ingestEndpoint.endsWith('/ingest')) {\n return ingestEndpoint.slice(0, -'/ingest'.length) + '/sessions';\n }\n return ingestEndpoint.replace(/\\/+$/, '') + '/sessions';\n}\n\nfunction postSession(state: SessionState, status: SessionStatus, durationMs?: number): void {\n const url = sessionsUrl(state.ctx.endpoint);\n const body = {\n session_id: state.id,\n status,\n release: state.ctx.release,\n environment: state.ctx.environment,\n app_version: state.ctx.appVersion,\n os_name: state.ctx.osName,\n user_id_anon: state.ctx.userIdAnon,\n duration_ms: durationMs,\n };\n for (const k of Object.keys(body) as (keyof typeof body)[]) {\n if (body[k] === undefined) delete body[k];\n }\n // Node 18+ has global fetch.\n fetch(url, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'X-Pionne-Token': state.ctx.token,\n },\n body: JSON.stringify(body),\n }).catch(() => undefined);\n}\n\nexport function startSession(ctx: SessionContext): string {\n current = {\n id: crypto.randomUUID(),\n startedAt: Date.now(),\n status: 'ok',\n ctx,\n };\n postSession(current, 'ok');\n\n // Best-effort 'exited' flip on graceful shutdown. We listen on 'exit'\n // (sync only — no async I/O guaranteed) and 'beforeExit' (where async\n // works). The actual session POST is fire-and-forget anyway.\n const onShutdown = () => {\n if (!current || current.status !== 'ok') return;\n flipSession('exited');\n };\n process.once('beforeExit', onShutdown);\n\n return current.id;\n}\n\nexport function flipSession(status: SessionStatus): void {\n if (!current) return;\n const rank: Record<SessionStatus, number> =\n { ok: 0, exited: 1, errored: 2, abnormal: 3, crashed: 4 };\n if (rank[status] <= rank[current.status]) return;\n current.status = status;\n postSession(current, status, Date.now() - current.startedAt);\n}\n\nexport function endSession(status: SessionStatus = 'exited'): void {\n if (!current) return;\n flipSession(status);\n current = null;\n}\n\nexport function getCurrentSessionId(): string | null {\n return current?.id ?? null;\n}\n\nexport function flipFromEvent(\n level: 'fatal' | 'error' | 'warning' | 'info' | undefined,\n mechanismType: string,\n): void {\n if (mechanismType === 'manual') return;\n if (level === 'fatal') flipSession('crashed');\n else if (level === 'error') flipSession('errored');\n}\n"],"mappings":"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","names":["process"]}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pionne/node",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Error monitoring SDK for Node.js — Pionne. Auto-captures uncaught exceptions and unhandled rejections, ships
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "Error monitoring SDK for Node.js — Pionne. Auto-captures uncaught exceptions and unhandled rejections, ships runtime context (Node, OS, hostname, pid), and tracks Release Health (crash-free user rate per release).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "AGKG Creations",
|
|
7
7
|
"homepage": "https://pionne.agkgcreations.fr",
|
package/src/index.ts
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import * as os from 'node:os';
|
|
2
2
|
import * as process from 'node:process';
|
|
3
3
|
|
|
4
|
+
import {
|
|
5
|
+
endSession as _endSession,
|
|
6
|
+
flipFromEvent,
|
|
7
|
+
getCurrentSessionId,
|
|
8
|
+
startSession as _startSession,
|
|
9
|
+
} from './sessions';
|
|
10
|
+
|
|
4
11
|
export type Level = 'fatal' | 'error' | 'warning' | 'info';
|
|
5
12
|
export type MechanismType =
|
|
6
13
|
| 'uncaughtException'
|
|
@@ -46,6 +53,12 @@ export interface PionneOptions {
|
|
|
46
53
|
userIdAnon?: string;
|
|
47
54
|
tags?: Record<string, string>;
|
|
48
55
|
maxStackFrames?: number;
|
|
56
|
+
/**
|
|
57
|
+
* Release Health — opens a session at init() with status='ok', flips to
|
|
58
|
+
* 'crashed'/'errored' if a fatal/error fires through the global handlers.
|
|
59
|
+
* The dashboard derives crash-free user rate per release. Default: true.
|
|
60
|
+
*/
|
|
61
|
+
releaseHealth?: boolean;
|
|
49
62
|
}
|
|
50
63
|
|
|
51
64
|
const DEFAULT_ENDPOINT = 'https://pionne.agkgcreations.fr/api/ingest';
|
|
@@ -54,7 +67,7 @@ const SDK_NAME = 'pionne.node';
|
|
|
54
67
|
const SDK_VERSION = '0.1.0';
|
|
55
68
|
|
|
56
69
|
type ResolvedConfig = Required<
|
|
57
|
-
Omit<PionneOptions, 'beforeSend' | 'userIdAnon' | 'tags' | 'release'>
|
|
70
|
+
Omit<PionneOptions, 'beforeSend' | 'userIdAnon' | 'tags' | 'release' | 'releaseHealth'>
|
|
58
71
|
> & {
|
|
59
72
|
beforeSend?: PionneOptions['beforeSend'];
|
|
60
73
|
userIdAnon?: string;
|
|
@@ -168,6 +181,7 @@ function installUncaughtHandler(): void {
|
|
|
168
181
|
// Fire-and-forget: process is going to die anyway. Best we can do is
|
|
169
182
|
// try to flush before exit, but Node will tear down imminently.
|
|
170
183
|
void send(event);
|
|
184
|
+
flipFromEvent(event.level, event.mechanism?.type ?? 'uncaughtException');
|
|
171
185
|
}
|
|
172
186
|
};
|
|
173
187
|
process.on('uncaughtException', onUncaught);
|
|
@@ -177,7 +191,10 @@ function installRejectionHandler(): void {
|
|
|
177
191
|
onRejection = (reason: unknown) => {
|
|
178
192
|
const err = reason instanceof Error ? reason : new Error(String(reason));
|
|
179
193
|
const event = buildEvent(err, 'error', 'unhandledRejection', false);
|
|
180
|
-
if (event)
|
|
194
|
+
if (event) {
|
|
195
|
+
void send(event);
|
|
196
|
+
flipFromEvent(event.level, event.mechanism?.type ?? 'unhandledRejection');
|
|
197
|
+
}
|
|
181
198
|
};
|
|
182
199
|
process.on('unhandledRejection', onRejection);
|
|
183
200
|
}
|
|
@@ -214,6 +231,19 @@ export const Pionne = {
|
|
|
214
231
|
|
|
215
232
|
if (config.captureUncaughtExceptions) installUncaughtHandler();
|
|
216
233
|
if (config.captureUnhandledRejections) installRejectionHandler();
|
|
234
|
+
|
|
235
|
+
// Release Health — open a session unless the host opted out.
|
|
236
|
+
if (options.releaseHealth !== false) {
|
|
237
|
+
_startSession({
|
|
238
|
+
endpoint: config.endpoint,
|
|
239
|
+
token: config.token,
|
|
240
|
+
release: config.release,
|
|
241
|
+
environment: config.environment,
|
|
242
|
+
appVersion: staticContext.app_version,
|
|
243
|
+
osName: staticContext.os_name,
|
|
244
|
+
userIdAnon: config.userIdAnon,
|
|
245
|
+
});
|
|
246
|
+
}
|
|
217
247
|
},
|
|
218
248
|
|
|
219
249
|
captureException(err: unknown, extra?: Partial<PionneEvent>): void {
|
|
@@ -265,6 +295,18 @@ export const Pionne = {
|
|
|
265
295
|
config = null;
|
|
266
296
|
staticContext = {};
|
|
267
297
|
},
|
|
298
|
+
|
|
299
|
+
// ─── Release Health ───────────────────────────────────────────────────
|
|
300
|
+
|
|
301
|
+
/** Manually end the current session (status='exited'). */
|
|
302
|
+
endSession(): void {
|
|
303
|
+
_endSession();
|
|
304
|
+
},
|
|
305
|
+
|
|
306
|
+
/** UUID of the current open session (for diagnostics). */
|
|
307
|
+
getSessionId(): string | null {
|
|
308
|
+
return getCurrentSessionId();
|
|
309
|
+
},
|
|
268
310
|
};
|
|
269
311
|
|
|
270
312
|
/**
|
package/src/sessions.ts
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// Release Health for Node.js. Same protocol as the browser/RN SDKs.
|
|
2
|
+
// We auto-flip on uncaughtException/unhandledRejection through the host
|
|
3
|
+
// SDK's existing handlers, and best-effort-emit 'exited' on process.exit.
|
|
4
|
+
|
|
5
|
+
import * as crypto from 'node:crypto';
|
|
6
|
+
|
|
7
|
+
export type SessionStatus = 'ok' | 'crashed' | 'errored' | 'abnormal' | 'exited';
|
|
8
|
+
|
|
9
|
+
export interface SessionContext {
|
|
10
|
+
endpoint: string;
|
|
11
|
+
token: string;
|
|
12
|
+
release?: string;
|
|
13
|
+
environment?: string;
|
|
14
|
+
appVersion?: string;
|
|
15
|
+
osName?: string;
|
|
16
|
+
userIdAnon?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface SessionState {
|
|
20
|
+
id: string;
|
|
21
|
+
startedAt: number;
|
|
22
|
+
status: SessionStatus;
|
|
23
|
+
ctx: SessionContext;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
let current: SessionState | null = null;
|
|
27
|
+
|
|
28
|
+
function sessionsUrl(ingestEndpoint: string): string {
|
|
29
|
+
if (ingestEndpoint.endsWith('/ingest')) {
|
|
30
|
+
return ingestEndpoint.slice(0, -'/ingest'.length) + '/sessions';
|
|
31
|
+
}
|
|
32
|
+
return ingestEndpoint.replace(/\/+$/, '') + '/sessions';
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function postSession(state: SessionState, status: SessionStatus, durationMs?: number): void {
|
|
36
|
+
const url = sessionsUrl(state.ctx.endpoint);
|
|
37
|
+
const body = {
|
|
38
|
+
session_id: state.id,
|
|
39
|
+
status,
|
|
40
|
+
release: state.ctx.release,
|
|
41
|
+
environment: state.ctx.environment,
|
|
42
|
+
app_version: state.ctx.appVersion,
|
|
43
|
+
os_name: state.ctx.osName,
|
|
44
|
+
user_id_anon: state.ctx.userIdAnon,
|
|
45
|
+
duration_ms: durationMs,
|
|
46
|
+
};
|
|
47
|
+
for (const k of Object.keys(body) as (keyof typeof body)[]) {
|
|
48
|
+
if (body[k] === undefined) delete body[k];
|
|
49
|
+
}
|
|
50
|
+
// Node 18+ has global fetch.
|
|
51
|
+
fetch(url, {
|
|
52
|
+
method: 'POST',
|
|
53
|
+
headers: {
|
|
54
|
+
'Content-Type': 'application/json',
|
|
55
|
+
'X-Pionne-Token': state.ctx.token,
|
|
56
|
+
},
|
|
57
|
+
body: JSON.stringify(body),
|
|
58
|
+
}).catch(() => undefined);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function startSession(ctx: SessionContext): string {
|
|
62
|
+
current = {
|
|
63
|
+
id: crypto.randomUUID(),
|
|
64
|
+
startedAt: Date.now(),
|
|
65
|
+
status: 'ok',
|
|
66
|
+
ctx,
|
|
67
|
+
};
|
|
68
|
+
postSession(current, 'ok');
|
|
69
|
+
|
|
70
|
+
// Best-effort 'exited' flip on graceful shutdown. We listen on 'exit'
|
|
71
|
+
// (sync only — no async I/O guaranteed) and 'beforeExit' (where async
|
|
72
|
+
// works). The actual session POST is fire-and-forget anyway.
|
|
73
|
+
const onShutdown = () => {
|
|
74
|
+
if (!current || current.status !== 'ok') return;
|
|
75
|
+
flipSession('exited');
|
|
76
|
+
};
|
|
77
|
+
process.once('beforeExit', onShutdown);
|
|
78
|
+
|
|
79
|
+
return current.id;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function flipSession(status: SessionStatus): void {
|
|
83
|
+
if (!current) return;
|
|
84
|
+
const rank: Record<SessionStatus, number> =
|
|
85
|
+
{ ok: 0, exited: 1, errored: 2, abnormal: 3, crashed: 4 };
|
|
86
|
+
if (rank[status] <= rank[current.status]) return;
|
|
87
|
+
current.status = status;
|
|
88
|
+
postSession(current, status, Date.now() - current.startedAt);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function endSession(status: SessionStatus = 'exited'): void {
|
|
92
|
+
if (!current) return;
|
|
93
|
+
flipSession(status);
|
|
94
|
+
current = null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function getCurrentSessionId(): string | null {
|
|
98
|
+
return current?.id ?? null;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function flipFromEvent(
|
|
102
|
+
level: 'fatal' | 'error' | 'warning' | 'info' | undefined,
|
|
103
|
+
mechanismType: string,
|
|
104
|
+
): void {
|
|
105
|
+
if (mechanismType === 'manual') return;
|
|
106
|
+
if (level === 'fatal') flipSession('crashed');
|
|
107
|
+
else if (level === 'error') flipSession('errored');
|
|
108
|
+
}
|