@pionne/node 0.3.1 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +48 -2
- package/dist/index.cjs +30 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.mts +12 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.mjs +30 -1
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/index.ts +55 -0
package/README.md
CHANGED
|
@@ -77,10 +77,56 @@ Pionne.setTags({ tier: 'pro' });
|
|
|
77
77
|
Pionne.setEnabled(false);
|
|
78
78
|
```
|
|
79
79
|
|
|
80
|
+
### Geography (opt-in)
|
|
81
|
+
|
|
82
|
+
Approximate server location (city, region, country) attached to every event,
|
|
83
|
+
just like Sentry. Off by default for privacy — flip `sendGeography` to enable:
|
|
84
|
+
|
|
85
|
+
```ts
|
|
86
|
+
Pionne.init({
|
|
87
|
+
token: 'pio_live_xxx',
|
|
88
|
+
sendGeography: true,
|
|
89
|
+
});
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Resolved once at startup via a free IP→geo lookup (`https://ipapi.co/json/`
|
|
93
|
+
by default), with a 4 s timeout. If the lookup fails the SDK silently keeps
|
|
94
|
+
shipping events without geo. Override the endpoint via `geographyEndpoint`
|
|
95
|
+
if you have your own.
|
|
96
|
+
|
|
80
97
|
## Options
|
|
81
98
|
|
|
82
|
-
Same shape as `@pionne/web` and `@pionne/react-native`.
|
|
83
|
-
|
|
99
|
+
Same shape as `@pionne/web` and `@pionne/react-native`. Highlights :
|
|
100
|
+
|
|
101
|
+
| Option | Type | Default |
|
|
102
|
+
| --------------------- | -------------------------- | ------------------------ |
|
|
103
|
+
| `token` | `string` (required) | — |
|
|
104
|
+
| `endpoint` | `string` | Pionne production |
|
|
105
|
+
| `release` | `string` | unset |
|
|
106
|
+
| `environment` | `string` | `NODE_ENV` ou `production` |
|
|
107
|
+
| `enabled` | `boolean` | `true` |
|
|
108
|
+
| `captureUncaughtErrors` | `boolean` | `true` |
|
|
109
|
+
| `captureUnhandledRejections` | `boolean` | `true` |
|
|
110
|
+
| `tags` | `Record<string, string>` | unset |
|
|
111
|
+
| `userIdAnon` | `string` | unset |
|
|
112
|
+
| `maxStackFrames` | `number` | `50` |
|
|
113
|
+
| `beforeSend` | `(event) => event \| null` | unset (drop if `null`) |
|
|
114
|
+
| `sendGeography` | `boolean` | `false` |
|
|
115
|
+
| `geographyEndpoint` | `string` | `https://ipapi.co/json/` |
|
|
116
|
+
| `releaseHealth` | `boolean` | `true` |
|
|
117
|
+
| `maxEventsPerSecond` | `number` | `10` |
|
|
118
|
+
|
|
119
|
+
### Notes
|
|
120
|
+
|
|
121
|
+
- **`maxEventsPerSecond`** — token-bucket process-wide. Au-delà, les events sont droppés silencieusement. Protège contre une boucle d'erreur dans un worker. `0` désactive (déconseillé sur des serveurs longue durée).
|
|
122
|
+
- **`releaseHealth`** — ouvre une session au `init()`, utile pour les services Node longue durée que tu veux mesurer comme un mobile (crash-free uptime).
|
|
123
|
+
- **`sendGeography`** — opt-in : résout city/region/country côté IP via [ipapi.co](https://ipapi.co/json/) par défaut. Sur un serveur, l'IP source est l'IP publique du serveur lui-même (pas celle d'un user). Customize via `geographyEndpoint` pour pointer sur ta propre lookup ou un proxy interne.
|
|
124
|
+
|
|
125
|
+
Voir les types TypeScript pour la liste complète.
|
|
126
|
+
|
|
127
|
+
## Rate limit serveur
|
|
128
|
+
|
|
129
|
+
L'API Pionne cap **600 req/min/token** (= 10/sec) sur tous les endpoints publics (`/ingest`, `/sessions`, `/feedback`). Au-delà → `HTTP 429` avec un header `Retry-After`. Le SDK fait silencieusement échouer (try/catch interne). Empêche un token leaké (ou un worker qui boucle) de drainer ton infra ou ton quota mensuel. Voir [doc rate limits](https://pionne.agkgcreations.fr/security/rate-limits).
|
|
84
130
|
|
|
85
131
|
## License
|
|
86
132
|
|
package/dist/index.cjs
CHANGED
|
@@ -156,6 +156,7 @@ function flipFromEvent(level, mechanismType) {
|
|
|
156
156
|
|
|
157
157
|
// src/index.ts
|
|
158
158
|
var DEFAULT_ENDPOINT = "https://pionne.agkgcreations.fr/api/ingest";
|
|
159
|
+
var DEFAULT_GEO_ENDPOINT = "https://ipapi.co/json/";
|
|
159
160
|
var DEFAULT_MAX_STACK = 50;
|
|
160
161
|
var SDK_NAME = "pionne.node";
|
|
161
162
|
var SDK_VERSION = "0.1.0";
|
|
@@ -248,6 +249,31 @@ async function send(event) {
|
|
|
248
249
|
} catch {
|
|
249
250
|
}
|
|
250
251
|
}
|
|
252
|
+
function fetchGeography(endpoint) {
|
|
253
|
+
if (typeof fetch !== "function") return;
|
|
254
|
+
const controller = typeof AbortController !== "undefined" ? new AbortController() : null;
|
|
255
|
+
const timeout = controller ? setTimeout(() => controller.abort(), 4e3) : null;
|
|
256
|
+
fetch(endpoint, {
|
|
257
|
+
method: "GET",
|
|
258
|
+
headers: { Accept: "application/json" },
|
|
259
|
+
signal: controller?.signal
|
|
260
|
+
}).then((res) => res.ok ? res.json() : null).then((data) => {
|
|
261
|
+
if (!data || typeof data !== "object") return;
|
|
262
|
+
const d = data;
|
|
263
|
+
const geo = {};
|
|
264
|
+
if (typeof d.city === "string") geo.city = d.city;
|
|
265
|
+
if (typeof d.region === "string") geo.region = d.region;
|
|
266
|
+
if (typeof d.country_name === "string") geo.country = d.country_name;
|
|
267
|
+
else if (typeof d.country === "string") geo.country = d.country;
|
|
268
|
+
if (typeof d.country_code === "string") geo.country_code = d.country_code;
|
|
269
|
+
if (Object.keys(geo).length === 0) return;
|
|
270
|
+
const ctx = staticContext.contexts ?? {};
|
|
271
|
+
staticContext.contexts = { ...ctx, geo };
|
|
272
|
+
}).catch(() => {
|
|
273
|
+
}).finally(() => {
|
|
274
|
+
if (timeout) clearTimeout(timeout);
|
|
275
|
+
});
|
|
276
|
+
}
|
|
251
277
|
function installUncaughtHandler() {
|
|
252
278
|
onUncaught = (err) => {
|
|
253
279
|
const event = buildEvent(err, "fatal", "uncaughtException", false);
|
|
@@ -300,10 +326,13 @@ var Pionne = {
|
|
|
300
326
|
beforeSend: options.beforeSend,
|
|
301
327
|
userIdAnon: options.userIdAnon,
|
|
302
328
|
tags: options.tags,
|
|
303
|
-
maxStackFrames: options.maxStackFrames ?? DEFAULT_MAX_STACK
|
|
329
|
+
maxStackFrames: options.maxStackFrames ?? DEFAULT_MAX_STACK,
|
|
330
|
+
sendGeography: options.sendGeography ?? false,
|
|
331
|
+
geographyEndpoint: options.geographyEndpoint ?? DEFAULT_GEO_ENDPOINT
|
|
304
332
|
};
|
|
305
333
|
if (config.captureUncaughtExceptions) installUncaughtHandler();
|
|
306
334
|
if (config.captureUnhandledRejections) installRejectionHandler();
|
|
335
|
+
if (config.sendGeography) fetchGeography(config.geographyEndpoint);
|
|
307
336
|
if (options.releaseHealth !== false) {
|
|
308
337
|
startSession({
|
|
309
338
|
endpoint: config.endpoint,
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/security.ts","../src/sessions.ts"],"sourcesContent":["import * as os from 'node:os';\nimport * as process from 'node:process';\n\nimport { RateLimiter, validateEndpoint, validateToken } from './security';\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 /** Token-bucket rate limit (events/sec). Default 10, set 0 to disable. */\n maxEventsPerSecond?: 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' | 'releaseHealth' | 'maxEventsPerSecond'>\n> & {\n beforeSend?: PionneOptions['beforeSend'];\n userIdAnon?: string;\n tags?: Record<string, string>;\n release?: string;\n};\n\nlet config: ResolvedConfig | null = null;\nlet rateLimiter: RateLimiter | null = null;\nlet droppedByRateLimit = 0;\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 (rateLimiter && !rateLimiter.allow()) {\n droppedByRateLimit++;\n if (process.env.NODE_ENV !== 'production' && droppedByRateLimit % 50 === 1) {\n console.warn(`[Pionne] rate-limit reached (${droppedByRateLimit} events dropped). Bump maxEventsPerSecond if intentional.`);\n }\n return;\n }\n\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 try {\n const isDev = process.env.NODE_ENV !== 'production';\n if (!options?.token || !validateToken(options.token)) {\n if (isDev) {\n console.warn('[Pionne] Missing or invalid token (expected pio_live_<≥16 chars>, no placeholders).');\n }\n return;\n }\n const endpoint = options.endpoint ?? DEFAULT_ENDPOINT;\n if (!validateEndpoint(endpoint, isDev)) {\n console.warn('[Pionne] Refusing non-HTTPS endpoint in production:', endpoint);\n return;\n }\n const rps = options.maxEventsPerSecond ?? 10;\n rateLimiter = rps > 0 ? new RateLimiter(rps, rps) : null;\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 } catch (e) {\n console.warn('[Pionne] init failed silently — monitoring disabled.', e);\n config = null;\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","// Mirror of @pionne/react-native and @pionne/web security guards.\n// Node-specific tweak: the \"localhost\" rule for non-HTTPS endpoints\n// also accepts hosts ending in `.local` (mDNS) — useful for staging\n// boxes behind Tailscale/ZeroTier.\n\nconst TOKEN_PREFIX = 'pio_live_';\nconst MIN_TOKEN_LENGTH = TOKEN_PREFIX.length + 16;\n\nexport function validateEndpoint(endpoint: string, isDev: boolean): boolean {\n try {\n const u = new URL(endpoint);\n if (u.protocol === 'https:') return true;\n if (u.protocol !== 'http:') return false;\n if (!isDev) return false;\n return /^(localhost|127\\.0\\.0\\.1|0\\.0\\.0\\.0|\\[::1\\]|.*\\.local)$/.test(u.hostname);\n } catch {\n return false;\n }\n}\n\nexport function validateToken(token: string): boolean {\n if (typeof token !== 'string') return false;\n if (!token.startsWith(TOKEN_PREFIX)) return false;\n if (token.length < MIN_TOKEN_LENGTH) return false;\n const lower = token.toLowerCase();\n for (const bad of ['xxx', 'yyy', 'todo', 'fixme', 'replace', 'changeme']) {\n if (lower.includes(bad)) return false;\n }\n return true;\n}\n\nexport class RateLimiter {\n private tokens: number;\n private lastRefill: number;\n constructor(\n private capacity: number,\n private refillPerSecond: number,\n ) {\n this.tokens = capacity;\n this.lastRefill = Date.now();\n }\n allow(): boolean {\n if (this.refillPerSecond <= 0) return true;\n const now = Date.now();\n const elapsedMs = now - this.lastRefill;\n if (elapsedMs > 0) {\n const refill = (elapsedMs / 1000) * this.refillPerSecond;\n this.tokens = Math.min(this.capacity, this.tokens + refill);\n this.lastRefill = now;\n }\n if (this.tokens >= 1) {\n this.tokens -= 1;\n return true;\n }\n return false;\n }\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"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/security.ts","../src/sessions.ts"],"sourcesContent":["import * as os from 'node:os';\nimport * as process from 'node:process';\n\nimport { RateLimiter, validateEndpoint, validateToken } from './security';\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 /** Token-bucket rate limit (events/sec). Default 10, set 0 to disable. */\n maxEventsPerSecond?: number;\n /**\n * Opt-in: resolve approximate server geography (city, region, country) once\n * at startup via a free IP→location lookup, and attach it to every event\n * under `contexts.geo`. Off by default for privacy.\n */\n sendGeography?: boolean;\n /**\n * Override the IP→geography endpoint. Must return JSON with at least\n * `city`, `region`, `country` (or `country_name`), and `country_code`\n * fields. Default: `https://ipapi.co/json/`.\n */\n geographyEndpoint?: string;\n}\n\nconst DEFAULT_ENDPOINT = 'https://pionne.agkgcreations.fr/api/ingest';\nconst DEFAULT_GEO_ENDPOINT = 'https://ipapi.co/json/';\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' | 'maxEventsPerSecond'>\n> & {\n beforeSend?: PionneOptions['beforeSend'];\n userIdAnon?: string;\n tags?: Record<string, string>;\n release?: string;\n};\n\nlet config: ResolvedConfig | null = null;\nlet rateLimiter: RateLimiter | null = null;\nlet droppedByRateLimit = 0;\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 (rateLimiter && !rateLimiter.allow()) {\n droppedByRateLimit++;\n if (process.env.NODE_ENV !== 'production' && droppedByRateLimit % 50 === 1) {\n console.warn(`[Pionne] rate-limit reached (${droppedByRateLimit} events dropped). Bump maxEventsPerSecond if intentional.`);\n }\n return;\n }\n\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\n/**\n * Fire-and-forget IP→geo lookup. Mutates `staticContext.contexts.geo` once it\n * resolves so subsequent events carry the location. Failures are silent — a\n * monitoring SDK must never crash or stall the host process.\n */\nfunction fetchGeography(endpoint: string): void {\n if (typeof fetch !== 'function') return;\n const controller =\n typeof AbortController !== 'undefined' ? new AbortController() : null;\n const timeout = controller\n ? setTimeout(() => controller.abort(), 4000)\n : null;\n fetch(endpoint, {\n method: 'GET',\n headers: { Accept: 'application/json' },\n signal: controller?.signal,\n })\n .then((res) => (res.ok ? res.json() : null))\n .then((data: unknown) => {\n if (!data || typeof data !== 'object') return;\n const d = data as Record<string, unknown>;\n const geo: Record<string, string> = {};\n if (typeof d.city === 'string') geo.city = d.city;\n if (typeof d.region === 'string') geo.region = d.region;\n if (typeof d.country_name === 'string') geo.country = d.country_name;\n else if (typeof d.country === 'string') geo.country = d.country;\n if (typeof d.country_code === 'string') geo.country_code = d.country_code;\n if (Object.keys(geo).length === 0) return;\n const ctx = staticContext.contexts ?? {};\n staticContext.contexts = { ...ctx, geo };\n })\n .catch(() => {\n // Best-effort: silently ignore lookup failures.\n })\n .finally(() => {\n if (timeout) clearTimeout(timeout);\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 try {\n const isDev = process.env.NODE_ENV !== 'production';\n if (!options?.token || !validateToken(options.token)) {\n if (isDev) {\n console.warn('[Pionne] Missing or invalid token (expected pio_live_<≥16 chars>, no placeholders).');\n }\n return;\n }\n const endpoint = options.endpoint ?? DEFAULT_ENDPOINT;\n if (!validateEndpoint(endpoint, isDev)) {\n console.warn('[Pionne] Refusing non-HTTPS endpoint in production:', endpoint);\n return;\n }\n const rps = options.maxEventsPerSecond ?? 10;\n rateLimiter = rps > 0 ? new RateLimiter(rps, rps) : null;\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 sendGeography: options.sendGeography ?? false,\n geographyEndpoint: options.geographyEndpoint ?? DEFAULT_GEO_ENDPOINT,\n };\n\n if (config.captureUncaughtExceptions) installUncaughtHandler();\n if (config.captureUnhandledRejections) installRejectionHandler();\n if (config.sendGeography) fetchGeography(config.geographyEndpoint);\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 } catch (e) {\n console.warn('[Pionne] init failed silently — monitoring disabled.', e);\n config = null;\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","// Mirror of @pionne/react-native and @pionne/web security guards.\n// Node-specific tweak: the \"localhost\" rule for non-HTTPS endpoints\n// also accepts hosts ending in `.local` (mDNS) — useful for staging\n// boxes behind Tailscale/ZeroTier.\n\nconst TOKEN_PREFIX = 'pio_live_';\nconst MIN_TOKEN_LENGTH = TOKEN_PREFIX.length + 16;\n\nexport function validateEndpoint(endpoint: string, isDev: boolean): boolean {\n try {\n const u = new URL(endpoint);\n if (u.protocol === 'https:') return true;\n if (u.protocol !== 'http:') return false;\n if (!isDev) return false;\n return /^(localhost|127\\.0\\.0\\.1|0\\.0\\.0\\.0|\\[::1\\]|.*\\.local)$/.test(u.hostname);\n } catch {\n return false;\n }\n}\n\nexport function validateToken(token: string): boolean {\n if (typeof token !== 'string') return false;\n if (!token.startsWith(TOKEN_PREFIX)) return false;\n if (token.length < MIN_TOKEN_LENGTH) return false;\n const lower = token.toLowerCase();\n for (const bad of ['xxx', 'yyy', 'todo', 'fixme', 'replace', 'changeme']) {\n if (lower.includes(bad)) return false;\n }\n return true;\n}\n\nexport class RateLimiter {\n private tokens: number;\n private lastRefill: number;\n constructor(\n private capacity: number,\n private refillPerSecond: number,\n ) {\n this.tokens = capacity;\n this.lastRefill = Date.now();\n }\n allow(): boolean {\n if (this.refillPerSecond <= 0) return true;\n const now = Date.now();\n const elapsedMs = now - this.lastRefill;\n if (elapsedMs > 0) {\n const refill = (elapsedMs / 1000) * this.refillPerSecond;\n this.tokens = Math.min(this.capacity, this.tokens + refill);\n this.lastRefill = now;\n }\n if (this.tokens >= 1) {\n this.tokens -= 1;\n return true;\n }\n return false;\n }\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
|
@@ -43,6 +43,18 @@ interface PionneOptions {
|
|
|
43
43
|
releaseHealth?: boolean;
|
|
44
44
|
/** Token-bucket rate limit (events/sec). Default 10, set 0 to disable. */
|
|
45
45
|
maxEventsPerSecond?: number;
|
|
46
|
+
/**
|
|
47
|
+
* Opt-in: resolve approximate server geography (city, region, country) once
|
|
48
|
+
* at startup via a free IP→location lookup, and attach it to every event
|
|
49
|
+
* under `contexts.geo`. Off by default for privacy.
|
|
50
|
+
*/
|
|
51
|
+
sendGeography?: boolean;
|
|
52
|
+
/**
|
|
53
|
+
* Override the IP→geography endpoint. Must return JSON with at least
|
|
54
|
+
* `city`, `region`, `country` (or `country_name`), and `country_code`
|
|
55
|
+
* fields. Default: `https://ipapi.co/json/`.
|
|
56
|
+
*/
|
|
57
|
+
geographyEndpoint?: string;
|
|
46
58
|
}
|
|
47
59
|
declare const Pionne: {
|
|
48
60
|
init(options: PionneOptions): void;
|
package/dist/index.d.ts
CHANGED
|
@@ -43,6 +43,18 @@ interface PionneOptions {
|
|
|
43
43
|
releaseHealth?: boolean;
|
|
44
44
|
/** Token-bucket rate limit (events/sec). Default 10, set 0 to disable. */
|
|
45
45
|
maxEventsPerSecond?: number;
|
|
46
|
+
/**
|
|
47
|
+
* Opt-in: resolve approximate server geography (city, region, country) once
|
|
48
|
+
* at startup via a free IP→location lookup, and attach it to every event
|
|
49
|
+
* under `contexts.geo`. Off by default for privacy.
|
|
50
|
+
*/
|
|
51
|
+
sendGeography?: boolean;
|
|
52
|
+
/**
|
|
53
|
+
* Override the IP→geography endpoint. Must return JSON with at least
|
|
54
|
+
* `city`, `region`, `country` (or `country_name`), and `country_code`
|
|
55
|
+
* fields. Default: `https://ipapi.co/json/`.
|
|
56
|
+
*/
|
|
57
|
+
geographyEndpoint?: string;
|
|
46
58
|
}
|
|
47
59
|
declare const Pionne: {
|
|
48
60
|
init(options: PionneOptions): void;
|
package/dist/index.mjs
CHANGED
|
@@ -121,6 +121,7 @@ function flipFromEvent(level, mechanismType) {
|
|
|
121
121
|
|
|
122
122
|
// src/index.ts
|
|
123
123
|
var DEFAULT_ENDPOINT = "https://pionne.agkgcreations.fr/api/ingest";
|
|
124
|
+
var DEFAULT_GEO_ENDPOINT = "https://ipapi.co/json/";
|
|
124
125
|
var DEFAULT_MAX_STACK = 50;
|
|
125
126
|
var SDK_NAME = "pionne.node";
|
|
126
127
|
var SDK_VERSION = "0.1.0";
|
|
@@ -213,6 +214,31 @@ async function send(event) {
|
|
|
213
214
|
} catch {
|
|
214
215
|
}
|
|
215
216
|
}
|
|
217
|
+
function fetchGeography(endpoint) {
|
|
218
|
+
if (typeof fetch !== "function") return;
|
|
219
|
+
const controller = typeof AbortController !== "undefined" ? new AbortController() : null;
|
|
220
|
+
const timeout = controller ? setTimeout(() => controller.abort(), 4e3) : null;
|
|
221
|
+
fetch(endpoint, {
|
|
222
|
+
method: "GET",
|
|
223
|
+
headers: { Accept: "application/json" },
|
|
224
|
+
signal: controller?.signal
|
|
225
|
+
}).then((res) => res.ok ? res.json() : null).then((data) => {
|
|
226
|
+
if (!data || typeof data !== "object") return;
|
|
227
|
+
const d = data;
|
|
228
|
+
const geo = {};
|
|
229
|
+
if (typeof d.city === "string") geo.city = d.city;
|
|
230
|
+
if (typeof d.region === "string") geo.region = d.region;
|
|
231
|
+
if (typeof d.country_name === "string") geo.country = d.country_name;
|
|
232
|
+
else if (typeof d.country === "string") geo.country = d.country;
|
|
233
|
+
if (typeof d.country_code === "string") geo.country_code = d.country_code;
|
|
234
|
+
if (Object.keys(geo).length === 0) return;
|
|
235
|
+
const ctx = staticContext.contexts ?? {};
|
|
236
|
+
staticContext.contexts = { ...ctx, geo };
|
|
237
|
+
}).catch(() => {
|
|
238
|
+
}).finally(() => {
|
|
239
|
+
if (timeout) clearTimeout(timeout);
|
|
240
|
+
});
|
|
241
|
+
}
|
|
216
242
|
function installUncaughtHandler() {
|
|
217
243
|
onUncaught = (err) => {
|
|
218
244
|
const event = buildEvent(err, "fatal", "uncaughtException", false);
|
|
@@ -265,10 +291,13 @@ var Pionne = {
|
|
|
265
291
|
beforeSend: options.beforeSend,
|
|
266
292
|
userIdAnon: options.userIdAnon,
|
|
267
293
|
tags: options.tags,
|
|
268
|
-
maxStackFrames: options.maxStackFrames ?? DEFAULT_MAX_STACK
|
|
294
|
+
maxStackFrames: options.maxStackFrames ?? DEFAULT_MAX_STACK,
|
|
295
|
+
sendGeography: options.sendGeography ?? false,
|
|
296
|
+
geographyEndpoint: options.geographyEndpoint ?? DEFAULT_GEO_ENDPOINT
|
|
269
297
|
};
|
|
270
298
|
if (config.captureUncaughtExceptions) installUncaughtHandler();
|
|
271
299
|
if (config.captureUnhandledRejections) installRejectionHandler();
|
|
300
|
+
if (config.sendGeography) fetchGeography(config.geographyEndpoint);
|
|
272
301
|
if (options.releaseHealth !== false) {
|
|
273
302
|
startSession({
|
|
274
303
|
endpoint: config.endpoint,
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/security.ts","../src/sessions.ts"],"sourcesContent":["import * as os from 'node:os';\nimport * as process from 'node:process';\n\nimport { RateLimiter, validateEndpoint, validateToken } from './security';\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 /** Token-bucket rate limit (events/sec). Default 10, set 0 to disable. */\n maxEventsPerSecond?: 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' | 'releaseHealth' | 'maxEventsPerSecond'>\n> & {\n beforeSend?: PionneOptions['beforeSend'];\n userIdAnon?: string;\n tags?: Record<string, string>;\n release?: string;\n};\n\nlet config: ResolvedConfig | null = null;\nlet rateLimiter: RateLimiter | null = null;\nlet droppedByRateLimit = 0;\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 (rateLimiter && !rateLimiter.allow()) {\n droppedByRateLimit++;\n if (process.env.NODE_ENV !== 'production' && droppedByRateLimit % 50 === 1) {\n console.warn(`[Pionne] rate-limit reached (${droppedByRateLimit} events dropped). Bump maxEventsPerSecond if intentional.`);\n }\n return;\n }\n\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 try {\n const isDev = process.env.NODE_ENV !== 'production';\n if (!options?.token || !validateToken(options.token)) {\n if (isDev) {\n console.warn('[Pionne] Missing or invalid token (expected pio_live_<≥16 chars>, no placeholders).');\n }\n return;\n }\n const endpoint = options.endpoint ?? DEFAULT_ENDPOINT;\n if (!validateEndpoint(endpoint, isDev)) {\n console.warn('[Pionne] Refusing non-HTTPS endpoint in production:', endpoint);\n return;\n }\n const rps = options.maxEventsPerSecond ?? 10;\n rateLimiter = rps > 0 ? new RateLimiter(rps, rps) : null;\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 } catch (e) {\n console.warn('[Pionne] init failed silently — monitoring disabled.', e);\n config = null;\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","// Mirror of @pionne/react-native and @pionne/web security guards.\n// Node-specific tweak: the \"localhost\" rule for non-HTTPS endpoints\n// also accepts hosts ending in `.local` (mDNS) — useful for staging\n// boxes behind Tailscale/ZeroTier.\n\nconst TOKEN_PREFIX = 'pio_live_';\nconst MIN_TOKEN_LENGTH = TOKEN_PREFIX.length + 16;\n\nexport function validateEndpoint(endpoint: string, isDev: boolean): boolean {\n try {\n const u = new URL(endpoint);\n if (u.protocol === 'https:') return true;\n if (u.protocol !== 'http:') return false;\n if (!isDev) return false;\n return /^(localhost|127\\.0\\.0\\.1|0\\.0\\.0\\.0|\\[::1\\]|.*\\.local)$/.test(u.hostname);\n } catch {\n return false;\n }\n}\n\nexport function validateToken(token: string): boolean {\n if (typeof token !== 'string') return false;\n if (!token.startsWith(TOKEN_PREFIX)) return false;\n if (token.length < MIN_TOKEN_LENGTH) return false;\n const lower = token.toLowerCase();\n for (const bad of ['xxx', 'yyy', 'todo', 'fixme', 'replace', 'changeme']) {\n if (lower.includes(bad)) return false;\n }\n return true;\n}\n\nexport class RateLimiter {\n private tokens: number;\n private lastRefill: number;\n constructor(\n private capacity: number,\n private refillPerSecond: number,\n ) {\n this.tokens = capacity;\n this.lastRefill = Date.now();\n }\n allow(): boolean {\n if (this.refillPerSecond <= 0) return true;\n const now = Date.now();\n const elapsedMs = now - this.lastRefill;\n if (elapsedMs > 0) {\n const refill = (elapsedMs / 1000) * this.refillPerSecond;\n this.tokens = Math.min(this.capacity, this.tokens + refill);\n this.lastRefill = now;\n }\n if (this.tokens >= 1) {\n this.tokens -= 1;\n return true;\n }\n return false;\n }\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"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/security.ts","../src/sessions.ts"],"sourcesContent":["import * as os from 'node:os';\nimport * as process from 'node:process';\n\nimport { RateLimiter, validateEndpoint, validateToken } from './security';\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 /** Token-bucket rate limit (events/sec). Default 10, set 0 to disable. */\n maxEventsPerSecond?: number;\n /**\n * Opt-in: resolve approximate server geography (city, region, country) once\n * at startup via a free IP→location lookup, and attach it to every event\n * under `contexts.geo`. Off by default for privacy.\n */\n sendGeography?: boolean;\n /**\n * Override the IP→geography endpoint. Must return JSON with at least\n * `city`, `region`, `country` (or `country_name`), and `country_code`\n * fields. Default: `https://ipapi.co/json/`.\n */\n geographyEndpoint?: string;\n}\n\nconst DEFAULT_ENDPOINT = 'https://pionne.agkgcreations.fr/api/ingest';\nconst DEFAULT_GEO_ENDPOINT = 'https://ipapi.co/json/';\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' | 'maxEventsPerSecond'>\n> & {\n beforeSend?: PionneOptions['beforeSend'];\n userIdAnon?: string;\n tags?: Record<string, string>;\n release?: string;\n};\n\nlet config: ResolvedConfig | null = null;\nlet rateLimiter: RateLimiter | null = null;\nlet droppedByRateLimit = 0;\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 (rateLimiter && !rateLimiter.allow()) {\n droppedByRateLimit++;\n if (process.env.NODE_ENV !== 'production' && droppedByRateLimit % 50 === 1) {\n console.warn(`[Pionne] rate-limit reached (${droppedByRateLimit} events dropped). Bump maxEventsPerSecond if intentional.`);\n }\n return;\n }\n\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\n/**\n * Fire-and-forget IP→geo lookup. Mutates `staticContext.contexts.geo` once it\n * resolves so subsequent events carry the location. Failures are silent — a\n * monitoring SDK must never crash or stall the host process.\n */\nfunction fetchGeography(endpoint: string): void {\n if (typeof fetch !== 'function') return;\n const controller =\n typeof AbortController !== 'undefined' ? new AbortController() : null;\n const timeout = controller\n ? setTimeout(() => controller.abort(), 4000)\n : null;\n fetch(endpoint, {\n method: 'GET',\n headers: { Accept: 'application/json' },\n signal: controller?.signal,\n })\n .then((res) => (res.ok ? res.json() : null))\n .then((data: unknown) => {\n if (!data || typeof data !== 'object') return;\n const d = data as Record<string, unknown>;\n const geo: Record<string, string> = {};\n if (typeof d.city === 'string') geo.city = d.city;\n if (typeof d.region === 'string') geo.region = d.region;\n if (typeof d.country_name === 'string') geo.country = d.country_name;\n else if (typeof d.country === 'string') geo.country = d.country;\n if (typeof d.country_code === 'string') geo.country_code = d.country_code;\n if (Object.keys(geo).length === 0) return;\n const ctx = staticContext.contexts ?? {};\n staticContext.contexts = { ...ctx, geo };\n })\n .catch(() => {\n // Best-effort: silently ignore lookup failures.\n })\n .finally(() => {\n if (timeout) clearTimeout(timeout);\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 try {\n const isDev = process.env.NODE_ENV !== 'production';\n if (!options?.token || !validateToken(options.token)) {\n if (isDev) {\n console.warn('[Pionne] Missing or invalid token (expected pio_live_<≥16 chars>, no placeholders).');\n }\n return;\n }\n const endpoint = options.endpoint ?? DEFAULT_ENDPOINT;\n if (!validateEndpoint(endpoint, isDev)) {\n console.warn('[Pionne] Refusing non-HTTPS endpoint in production:', endpoint);\n return;\n }\n const rps = options.maxEventsPerSecond ?? 10;\n rateLimiter = rps > 0 ? new RateLimiter(rps, rps) : null;\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 sendGeography: options.sendGeography ?? false,\n geographyEndpoint: options.geographyEndpoint ?? DEFAULT_GEO_ENDPOINT,\n };\n\n if (config.captureUncaughtExceptions) installUncaughtHandler();\n if (config.captureUnhandledRejections) installRejectionHandler();\n if (config.sendGeography) fetchGeography(config.geographyEndpoint);\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 } catch (e) {\n console.warn('[Pionne] init failed silently — monitoring disabled.', e);\n config = null;\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","// Mirror of @pionne/react-native and @pionne/web security guards.\n// Node-specific tweak: the \"localhost\" rule for non-HTTPS endpoints\n// also accepts hosts ending in `.local` (mDNS) — useful for staging\n// boxes behind Tailscale/ZeroTier.\n\nconst TOKEN_PREFIX = 'pio_live_';\nconst MIN_TOKEN_LENGTH = TOKEN_PREFIX.length + 16;\n\nexport function validateEndpoint(endpoint: string, isDev: boolean): boolean {\n try {\n const u = new URL(endpoint);\n if (u.protocol === 'https:') return true;\n if (u.protocol !== 'http:') return false;\n if (!isDev) return false;\n return /^(localhost|127\\.0\\.0\\.1|0\\.0\\.0\\.0|\\[::1\\]|.*\\.local)$/.test(u.hostname);\n } catch {\n return false;\n }\n}\n\nexport function validateToken(token: string): boolean {\n if (typeof token !== 'string') return false;\n if (!token.startsWith(TOKEN_PREFIX)) return false;\n if (token.length < MIN_TOKEN_LENGTH) return false;\n const lower = token.toLowerCase();\n for (const bad of ['xxx', 'yyy', 'todo', 'fixme', 'replace', 'changeme']) {\n if (lower.includes(bad)) return false;\n }\n return true;\n}\n\nexport class RateLimiter {\n private tokens: number;\n private lastRefill: number;\n constructor(\n private capacity: number,\n private refillPerSecond: number,\n ) {\n this.tokens = capacity;\n this.lastRefill = Date.now();\n }\n allow(): boolean {\n if (this.refillPerSecond <= 0) return true;\n const now = Date.now();\n const elapsedMs = now - this.lastRefill;\n if (elapsedMs > 0) {\n const refill = (elapsedMs / 1000) * this.refillPerSecond;\n this.tokens = Math.min(this.capacity, this.tokens + refill);\n this.lastRefill = now;\n }\n if (this.tokens >= 1) {\n this.tokens -= 1;\n return true;\n }\n return false;\n }\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,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pionne/node",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
4
4
|
"description": "Error monitoring SDK for Node.js \u2014 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",
|
package/src/index.ts
CHANGED
|
@@ -62,9 +62,22 @@ export interface PionneOptions {
|
|
|
62
62
|
releaseHealth?: boolean;
|
|
63
63
|
/** Token-bucket rate limit (events/sec). Default 10, set 0 to disable. */
|
|
64
64
|
maxEventsPerSecond?: number;
|
|
65
|
+
/**
|
|
66
|
+
* Opt-in: resolve approximate server geography (city, region, country) once
|
|
67
|
+
* at startup via a free IP→location lookup, and attach it to every event
|
|
68
|
+
* under `contexts.geo`. Off by default for privacy.
|
|
69
|
+
*/
|
|
70
|
+
sendGeography?: boolean;
|
|
71
|
+
/**
|
|
72
|
+
* Override the IP→geography endpoint. Must return JSON with at least
|
|
73
|
+
* `city`, `region`, `country` (or `country_name`), and `country_code`
|
|
74
|
+
* fields. Default: `https://ipapi.co/json/`.
|
|
75
|
+
*/
|
|
76
|
+
geographyEndpoint?: string;
|
|
65
77
|
}
|
|
66
78
|
|
|
67
79
|
const DEFAULT_ENDPOINT = 'https://pionne.agkgcreations.fr/api/ingest';
|
|
80
|
+
const DEFAULT_GEO_ENDPOINT = 'https://ipapi.co/json/';
|
|
68
81
|
const DEFAULT_MAX_STACK = 50;
|
|
69
82
|
const SDK_NAME = 'pionne.node';
|
|
70
83
|
const SDK_VERSION = '0.1.0';
|
|
@@ -187,6 +200,45 @@ async function send(event: PionneEvent): Promise<void> {
|
|
|
187
200
|
}
|
|
188
201
|
}
|
|
189
202
|
|
|
203
|
+
/**
|
|
204
|
+
* Fire-and-forget IP→geo lookup. Mutates `staticContext.contexts.geo` once it
|
|
205
|
+
* resolves so subsequent events carry the location. Failures are silent — a
|
|
206
|
+
* monitoring SDK must never crash or stall the host process.
|
|
207
|
+
*/
|
|
208
|
+
function fetchGeography(endpoint: string): void {
|
|
209
|
+
if (typeof fetch !== 'function') return;
|
|
210
|
+
const controller =
|
|
211
|
+
typeof AbortController !== 'undefined' ? new AbortController() : null;
|
|
212
|
+
const timeout = controller
|
|
213
|
+
? setTimeout(() => controller.abort(), 4000)
|
|
214
|
+
: null;
|
|
215
|
+
fetch(endpoint, {
|
|
216
|
+
method: 'GET',
|
|
217
|
+
headers: { Accept: 'application/json' },
|
|
218
|
+
signal: controller?.signal,
|
|
219
|
+
})
|
|
220
|
+
.then((res) => (res.ok ? res.json() : null))
|
|
221
|
+
.then((data: unknown) => {
|
|
222
|
+
if (!data || typeof data !== 'object') return;
|
|
223
|
+
const d = data as Record<string, unknown>;
|
|
224
|
+
const geo: Record<string, string> = {};
|
|
225
|
+
if (typeof d.city === 'string') geo.city = d.city;
|
|
226
|
+
if (typeof d.region === 'string') geo.region = d.region;
|
|
227
|
+
if (typeof d.country_name === 'string') geo.country = d.country_name;
|
|
228
|
+
else if (typeof d.country === 'string') geo.country = d.country;
|
|
229
|
+
if (typeof d.country_code === 'string') geo.country_code = d.country_code;
|
|
230
|
+
if (Object.keys(geo).length === 0) return;
|
|
231
|
+
const ctx = staticContext.contexts ?? {};
|
|
232
|
+
staticContext.contexts = { ...ctx, geo };
|
|
233
|
+
})
|
|
234
|
+
.catch(() => {
|
|
235
|
+
// Best-effort: silently ignore lookup failures.
|
|
236
|
+
})
|
|
237
|
+
.finally(() => {
|
|
238
|
+
if (timeout) clearTimeout(timeout);
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
|
|
190
242
|
function installUncaughtHandler(): void {
|
|
191
243
|
onUncaught = (err: Error) => {
|
|
192
244
|
const event = buildEvent(err, 'fatal', 'uncaughtException', false);
|
|
@@ -247,10 +299,13 @@ export const Pionne = {
|
|
|
247
299
|
userIdAnon: options.userIdAnon,
|
|
248
300
|
tags: options.tags,
|
|
249
301
|
maxStackFrames: options.maxStackFrames ?? DEFAULT_MAX_STACK,
|
|
302
|
+
sendGeography: options.sendGeography ?? false,
|
|
303
|
+
geographyEndpoint: options.geographyEndpoint ?? DEFAULT_GEO_ENDPOINT,
|
|
250
304
|
};
|
|
251
305
|
|
|
252
306
|
if (config.captureUncaughtExceptions) installUncaughtHandler();
|
|
253
307
|
if (config.captureUnhandledRejections) installRejectionHandler();
|
|
308
|
+
if (config.sendGeography) fetchGeography(config.geographyEndpoint);
|
|
254
309
|
|
|
255
310
|
// Release Health — open a session unless the host opted out.
|
|
256
311
|
if (options.releaseHealth !== false) {
|