@interfere/node 0.0.1-canary.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +30 -0
- package/dist/capture.d.mts +34 -0
- package/dist/capture.d.mts.map +1 -0
- package/dist/capture.mjs +85 -0
- package/dist/capture.mjs.map +1 -0
- package/dist/index.d.mts +4 -0
- package/dist/index.mjs +3 -0
- package/dist/init.d.mts +61 -0
- package/dist/init.d.mts.map +1 -0
- package/dist/init.mjs +87 -0
- package/dist/init.mjs.map +1 -0
- package/dist/internal/dedupe.d.mts +5 -0
- package/dist/internal/dedupe.d.mts.map +1 -0
- package/dist/internal/dedupe.mjs +11 -0
- package/dist/internal/dedupe.mjs.map +1 -0
- package/dist/internal/env.d.mts +20 -0
- package/dist/internal/env.d.mts.map +1 -0
- package/dist/internal/env.mjs +25 -0
- package/dist/internal/env.mjs.map +1 -0
- package/dist/internal/global-handlers.d.mts +25 -0
- package/dist/internal/global-handlers.d.mts.map +1 -0
- package/dist/internal/global-handlers.mjs +68 -0
- package/dist/internal/global-handlers.mjs.map +1 -0
- package/dist/internal/normalize-request.d.mts +15 -0
- package/dist/internal/normalize-request.d.mts.map +1 -0
- package/dist/internal/normalize-request.mjs +58 -0
- package/dist/internal/normalize-request.mjs.map +1 -0
- package/dist/internal/release-slug.d.mts +8 -0
- package/dist/internal/release-slug.d.mts.map +1 -0
- package/dist/internal/release-slug.mjs +32 -0
- package/dist/internal/release-slug.mjs.map +1 -0
- package/dist/internal/remote-config.d.mts +5 -0
- package/dist/internal/remote-config.d.mts.map +1 -0
- package/dist/internal/remote-config.mjs +29 -0
- package/dist/internal/remote-config.mjs.map +1 -0
- package/dist/internal/types.d.mts +16 -0
- package/dist/internal/types.d.mts.map +1 -0
- package/dist/internal/types.mjs +1 -0
- package/dist/internal/version.d.mts +4 -0
- package/dist/internal/version.d.mts.map +1 -0
- package/dist/internal/version.mjs +5 -0
- package/dist/internal/version.mjs.map +1 -0
- package/dist/package.mjs +5 -0
- package/dist/package.mjs.map +1 -0
- package/package.json +74 -0
package/README.md
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# @interfere/node
|
|
2
|
+
|
|
3
|
+
> **Status: canary.** Pre-1.0 API, expect breaking changes. Pin to a specific `10.0.0-canary.N` in CI.
|
|
4
|
+
|
|
5
|
+
Node.js SDK for [Interfere](https://interfere.com).
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
bun add @interfere/node
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import { init, captureError, withSpan } from "@interfere/node";
|
|
17
|
+
|
|
18
|
+
await init({ serviceName: "checkout-worker" });
|
|
19
|
+
|
|
20
|
+
await withSpan("checkout.fulfill", () => fulfill(order));
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
await doWork();
|
|
24
|
+
} catch (err) {
|
|
25
|
+
captureError(err);
|
|
26
|
+
throw err;
|
|
27
|
+
}
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Set `INTERFERE_API_KEY` in the environment. Full docs at <https://interfere.com/docs>.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { CaptureErrorContext } from "./internal/types.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/capture.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Reports an error to Interfere. Safe to call from anywhere — no-op when the
|
|
6
|
+
* SDK is not configured (`INTERFERE_API_KEY` unset) or when the `errors`
|
|
7
|
+
* plugin is disabled by remote config.
|
|
8
|
+
*
|
|
9
|
+
* Internally opens a short OTel span and records the error as an `exception`
|
|
10
|
+
* event on it. The span ships through the same `OTLPTraceExporter` wired up
|
|
11
|
+
* in `init()`, so error capture inherits the OTel transport (batch, retry,
|
|
12
|
+
* traceparent propagation) without a separate fetch path. The collector's
|
|
13
|
+
* `mapTracesToSpanEvents` lifts the OTel-semconv `exception.*` attrs plus
|
|
14
|
+
* our `interfere.exception.*` taxonomy into the `span_events` datasource.
|
|
15
|
+
*
|
|
16
|
+
* Pass the request (a `Request`, a Node `IncomingMessage`-like object, or
|
|
17
|
+
* any object with `headers`/`method`/`url`) so the span carries request
|
|
18
|
+
* context. If there's an active OTel context (e.g. an HTTP handler span),
|
|
19
|
+
* the exception attaches to it as a child; otherwise it becomes a
|
|
20
|
+
* root span.
|
|
21
|
+
*/
|
|
22
|
+
declare function captureError(error: unknown, request?: unknown, errorContext?: CaptureErrorContext): void;
|
|
23
|
+
/**
|
|
24
|
+
* Reports a string message as an error event.
|
|
25
|
+
*/
|
|
26
|
+
declare function captureMessage(message: string, request?: unknown, errorContext?: CaptureErrorContext): void;
|
|
27
|
+
/**
|
|
28
|
+
* Wraps a function in an OTel span. Thrown errors are recorded on the span
|
|
29
|
+
* and rethrown. Without an active provider (i.e. before `init()`), still
|
|
30
|
+
* runs `fn` but produces no telemetry.
|
|
31
|
+
*/
|
|
32
|
+
declare function withSpan<T>(name: string, fn: () => Promise<T> | T): Promise<T>;
|
|
33
|
+
//#endregion
|
|
34
|
+
export { type CaptureErrorContext, captureError, captureMessage, withSpan };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"capture.d.mts","names":[],"sources":["../src/capture.ts"],"mappings":";;;;AAwCA;;;;;;;;;;AA8EA;;;;;;;iBA9EgB,YAAA,CACd,KAAA,WACA,OAAA,YACA,YAAA,GAAe,mBAAA;;;AAwFjB;iBAbgB,cAAA,CACd,OAAA,UACA,OAAA,YACA,YAAA,GAAe,mBAAA;;;;;;iBAUK,QAAA,GAAA,CACpB,IAAA,UACA,EAAA,QAAU,OAAA,CAAQ,CAAA,IAAK,CAAA,GACtB,OAAA,CAAQ,CAAA"}
|
package/dist/capture.mjs
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { isErrorCaptured, markErrorCaptured } from "./internal/dedupe.mjs";
|
|
2
|
+
import { isEnabledInEnvironment } from "./internal/env.mjs";
|
|
3
|
+
import { normalizeRequest } from "./internal/normalize-request.mjs";
|
|
4
|
+
import { isPluginEnabled } from "./internal/remote-config.mjs";
|
|
5
|
+
import { MECHANISM_TYPE, toError } from "@interfere/types/sdk/errors";
|
|
6
|
+
import { SpanKind, SpanStatusCode, context, trace } from "@opentelemetry/api";
|
|
7
|
+
//#region src/capture.ts
|
|
8
|
+
const TRACER_NAME = "@interfere/node";
|
|
9
|
+
const ERROR_SPAN_NAME = "interfere.error.captured";
|
|
10
|
+
const SESSION_HEADER = "x-interfere-session";
|
|
11
|
+
const DEFAULT_ERROR_MECHANISM = {
|
|
12
|
+
type: MECHANISM_TYPE.node.captureError,
|
|
13
|
+
handled: true
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Reports an error to Interfere. Safe to call from anywhere — no-op when the
|
|
17
|
+
* SDK is not configured (`INTERFERE_API_KEY` unset) or when the `errors`
|
|
18
|
+
* plugin is disabled by remote config.
|
|
19
|
+
*
|
|
20
|
+
* Internally opens a short OTel span and records the error as an `exception`
|
|
21
|
+
* event on it. The span ships through the same `OTLPTraceExporter` wired up
|
|
22
|
+
* in `init()`, so error capture inherits the OTel transport (batch, retry,
|
|
23
|
+
* traceparent propagation) without a separate fetch path. The collector's
|
|
24
|
+
* `mapTracesToSpanEvents` lifts the OTel-semconv `exception.*` attrs plus
|
|
25
|
+
* our `interfere.exception.*` taxonomy into the `span_events` datasource.
|
|
26
|
+
*
|
|
27
|
+
* Pass the request (a `Request`, a Node `IncomingMessage`-like object, or
|
|
28
|
+
* any object with `headers`/`method`/`url`) so the span carries request
|
|
29
|
+
* context. If there's an active OTel context (e.g. an HTTP handler span),
|
|
30
|
+
* the exception attaches to it as a child; otherwise it becomes a
|
|
31
|
+
* root span.
|
|
32
|
+
*/
|
|
33
|
+
function captureError(error, request, errorContext) {
|
|
34
|
+
if (!isEnabledInEnvironment()) return;
|
|
35
|
+
if (!isPluginEnabled("errors")) return;
|
|
36
|
+
if (isErrorCaptured(error)) return;
|
|
37
|
+
markErrorCaptured(error);
|
|
38
|
+
const err = toError(error);
|
|
39
|
+
const normalizedRequest = normalizeRequest(request);
|
|
40
|
+
const mechanism = errorContext?.mechanism ?? DEFAULT_ERROR_MECHANISM;
|
|
41
|
+
const span = trace.getTracer(TRACER_NAME).startSpan(ERROR_SPAN_NAME, { kind: SpanKind.INTERNAL }, context.active());
|
|
42
|
+
const requestMethod = errorContext?.node?.requestMethod ?? normalizedRequest?.method ?? null;
|
|
43
|
+
if (requestMethod) span.setAttribute("http.request.method", requestMethod);
|
|
44
|
+
const requestPath = errorContext?.node?.requestPath ?? normalizedRequest?.path ?? null;
|
|
45
|
+
if (requestPath) span.setAttribute("url.path", requestPath);
|
|
46
|
+
const sessionId = normalizedRequest?.headers.get(SESSION_HEADER);
|
|
47
|
+
if (sessionId) span.setAttribute("interfere.session.id", sessionId);
|
|
48
|
+
const eventAttrs = {
|
|
49
|
+
"exception.type": err.name,
|
|
50
|
+
"exception.message": err.message,
|
|
51
|
+
"interfere.exception.mechanism": mechanism.type,
|
|
52
|
+
"interfere.exception.handled": mechanism.handled
|
|
53
|
+
};
|
|
54
|
+
if (err.stack) eventAttrs["exception.stacktrace"] = err.stack;
|
|
55
|
+
const digest = errorContext?.node?.errorDigest;
|
|
56
|
+
if (digest) eventAttrs["interfere.error.digest"] = digest;
|
|
57
|
+
span.addEvent("exception", eventAttrs);
|
|
58
|
+
span.setStatus({ code: SpanStatusCode.ERROR });
|
|
59
|
+
span.end();
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Reports a string message as an error event.
|
|
63
|
+
*/
|
|
64
|
+
function captureMessage(message, request, errorContext) {
|
|
65
|
+
captureError(new Error(message), request, errorContext);
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Wraps a function in an OTel span. Thrown errors are recorded on the span
|
|
69
|
+
* and rethrown. Without an active provider (i.e. before `init()`), still
|
|
70
|
+
* runs `fn` but produces no telemetry.
|
|
71
|
+
*/
|
|
72
|
+
async function withSpan(name, fn) {
|
|
73
|
+
const span = trace.getTracer(TRACER_NAME).startSpan(name);
|
|
74
|
+
try {
|
|
75
|
+
return await context.with(trace.setSpan(context.active(), span), fn);
|
|
76
|
+
} catch (error) {
|
|
77
|
+
span.recordException(error);
|
|
78
|
+
span.setStatus({ code: SpanStatusCode.ERROR });
|
|
79
|
+
throw error;
|
|
80
|
+
} finally {
|
|
81
|
+
span.end();
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
//#endregion
|
|
85
|
+
export { captureError, captureMessage, withSpan };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"capture.mjs","names":[],"sources":["../src/capture.ts"],"sourcesContent":["import { MECHANISM_TYPE, toError } from \"@interfere/types/sdk/errors\";\nimport type { ErrorMechanism } from \"@interfere/types/sdk/plugins/payload/errors\";\n\nimport { context, SpanKind, SpanStatusCode, trace } from \"@opentelemetry/api\";\n\nimport { isErrorCaptured, markErrorCaptured } from \"./internal/dedupe.js\";\nimport { isEnabledInEnvironment } from \"./internal/env.js\";\nimport { normalizeRequest } from \"./internal/normalize-request.js\";\nimport { isPluginEnabled } from \"./internal/remote-config.js\";\nimport type { CaptureErrorContext } from \"./internal/types.js\";\n\nexport type { CaptureErrorContext } from \"./internal/types.js\";\n\nconst TRACER_NAME = \"@interfere/node\";\nconst ERROR_SPAN_NAME = \"interfere.error.captured\";\nconst SESSION_HEADER = \"x-interfere-session\";\n\nconst DEFAULT_ERROR_MECHANISM: ErrorMechanism = {\n type: MECHANISM_TYPE.node.captureError,\n handled: true,\n};\n\n/**\n * Reports an error to Interfere. Safe to call from anywhere — no-op when the\n * SDK is not configured (`INTERFERE_API_KEY` unset) or when the `errors`\n * plugin is disabled by remote config.\n *\n * Internally opens a short OTel span and records the error as an `exception`\n * event on it. The span ships through the same `OTLPTraceExporter` wired up\n * in `init()`, so error capture inherits the OTel transport (batch, retry,\n * traceparent propagation) without a separate fetch path. The collector's\n * `mapTracesToSpanEvents` lifts the OTel-semconv `exception.*` attrs plus\n * our `interfere.exception.*` taxonomy into the `span_events` datasource.\n *\n * Pass the request (a `Request`, a Node `IncomingMessage`-like object, or\n * any object with `headers`/`method`/`url`) so the span carries request\n * context. If there's an active OTel context (e.g. an HTTP handler span),\n * the exception attaches to it as a child; otherwise it becomes a\n * root span.\n */\nexport function captureError(\n error: unknown,\n request?: unknown,\n errorContext?: CaptureErrorContext\n): void {\n if (!isEnabledInEnvironment()) {\n return;\n }\n\n if (!isPluginEnabled(\"errors\")) {\n return;\n }\n\n if (isErrorCaptured(error)) {\n return;\n }\n markErrorCaptured(error);\n\n const err = toError(error);\n const normalizedRequest = normalizeRequest(request);\n const mechanism = errorContext?.mechanism ?? DEFAULT_ERROR_MECHANISM;\n\n const tracer = trace.getTracer(TRACER_NAME);\n const span = tracer.startSpan(\n ERROR_SPAN_NAME,\n { kind: SpanKind.INTERNAL },\n context.active()\n );\n\n // Request context as span attributes (OTel HTTP semconv). Prefer an\n // explicit `errorContext.node` payload over the normalized request when\n // both are present — callers passing `node` are typically frameworks\n // (Next.js error handlers) that already have the canonical values.\n const requestMethod =\n errorContext?.node?.requestMethod ?? normalizedRequest?.method ?? null;\n if (requestMethod) {\n span.setAttribute(\"http.request.method\", requestMethod);\n }\n const requestPath =\n errorContext?.node?.requestPath ?? normalizedRequest?.path ?? null;\n if (requestPath) {\n span.setAttribute(\"url.path\", requestPath);\n }\n\n // Session header is the legacy bridge: browser/Next forwards\n // `x-interfere-session` so the dashboard can stitch server errors to a\n // user session. Lift it as a span attribute so the mapper can fold it in.\n const sessionId = normalizedRequest?.headers.get(SESSION_HEADER);\n if (sessionId) {\n span.setAttribute(\"interfere.session.id\", sessionId);\n }\n\n // Exception event — OTel semconv naming (`exception.{type,message,\n // stacktrace}`) plus our taxonomy (`interfere.exception.{mechanism,\n // handled}`, `interfere.error.digest`). `mapTracesToSpanEvents` in the\n // collector lifts both sets into typed columns on the `span_events` row.\n const eventAttrs: Record<string, string | boolean> = {\n \"exception.type\": err.name,\n \"exception.message\": err.message,\n \"interfere.exception.mechanism\": mechanism.type,\n \"interfere.exception.handled\": mechanism.handled,\n };\n if (err.stack) {\n eventAttrs[\"exception.stacktrace\"] = err.stack;\n }\n const digest = errorContext?.node?.errorDigest;\n if (digest) {\n eventAttrs[\"interfere.error.digest\"] = digest;\n }\n\n span.addEvent(\"exception\", eventAttrs);\n span.setStatus({ code: SpanStatusCode.ERROR });\n span.end();\n}\n\n/**\n * Reports a string message as an error event.\n */\nexport function captureMessage(\n message: string,\n request?: unknown,\n errorContext?: CaptureErrorContext\n): void {\n captureError(new Error(message), request, errorContext);\n}\n\n/**\n * Wraps a function in an OTel span. Thrown errors are recorded on the span\n * and rethrown. Without an active provider (i.e. before `init()`), still\n * runs `fn` but produces no telemetry.\n */\nexport async function withSpan<T>(\n name: string,\n fn: () => Promise<T> | T\n): Promise<T> {\n const tracer = trace.getTracer(TRACER_NAME);\n const span = tracer.startSpan(name);\n try {\n return await context.with(trace.setSpan(context.active(), span), fn);\n } catch (error) {\n span.recordException(error as Error);\n span.setStatus({ code: SpanStatusCode.ERROR });\n throw error;\n } finally {\n span.end();\n }\n}\n"],"mappings":";;;;;;;AAaA,MAAM,cAAc;AACpB,MAAM,kBAAkB;AACxB,MAAM,iBAAiB;AAEvB,MAAM,0BAA0C;CAC9C,MAAM,eAAe,KAAK;CAC1B,SAAS;CACV;;;;;;;;;;;;;;;;;;;AAoBD,SAAgB,aACd,OACA,SACA,cACM;CACN,IAAI,CAAC,wBAAwB,EAC3B;CAGF,IAAI,CAAC,gBAAgB,SAAS,EAC5B;CAGF,IAAI,gBAAgB,MAAM,EACxB;CAEF,kBAAkB,MAAM;CAExB,MAAM,MAAM,QAAQ,MAAM;CAC1B,MAAM,oBAAoB,iBAAiB,QAAQ;CACnD,MAAM,YAAY,cAAc,aAAa;CAG7C,MAAM,OADS,MAAM,UAAU,YACZ,CAAC,UAClB,iBACA,EAAE,MAAM,SAAS,UAAU,EAC3B,QAAQ,QAAQ,CACjB;CAMD,MAAM,gBACJ,cAAc,MAAM,iBAAiB,mBAAmB,UAAU;CACpE,IAAI,eACF,KAAK,aAAa,uBAAuB,cAAc;CAEzD,MAAM,cACJ,cAAc,MAAM,eAAe,mBAAmB,QAAQ;CAChE,IAAI,aACF,KAAK,aAAa,YAAY,YAAY;CAM5C,MAAM,YAAY,mBAAmB,QAAQ,IAAI,eAAe;CAChE,IAAI,WACF,KAAK,aAAa,wBAAwB,UAAU;CAOtD,MAAM,aAA+C;EACnD,kBAAkB,IAAI;EACtB,qBAAqB,IAAI;EACzB,iCAAiC,UAAU;EAC3C,+BAA+B,UAAU;EAC1C;CACD,IAAI,IAAI,OACN,WAAW,0BAA0B,IAAI;CAE3C,MAAM,SAAS,cAAc,MAAM;CACnC,IAAI,QACF,WAAW,4BAA4B;CAGzC,KAAK,SAAS,aAAa,WAAW;CACtC,KAAK,UAAU,EAAE,MAAM,eAAe,OAAO,CAAC;CAC9C,KAAK,KAAK;;;;;AAMZ,SAAgB,eACd,SACA,SACA,cACM;CACN,aAAa,IAAI,MAAM,QAAQ,EAAE,SAAS,aAAa;;;;;;;AAQzD,eAAsB,SACpB,MACA,IACY;CAEZ,MAAM,OADS,MAAM,UAAU,YACZ,CAAC,UAAU,KAAK;CACnC,IAAI;EACF,OAAO,MAAM,QAAQ,KAAK,MAAM,QAAQ,QAAQ,QAAQ,EAAE,KAAK,EAAE,GAAG;UAC7D,OAAO;EACd,KAAK,gBAAgB,MAAe;EACpC,KAAK,UAAU,EAAE,MAAM,eAAe,OAAO,CAAC;EAC9C,MAAM;WACE;EACR,KAAK,KAAK"}
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { CaptureErrorContext } from "./internal/types.mjs";
|
|
2
|
+
import { captureError, captureMessage, withSpan } from "./capture.mjs";
|
|
3
|
+
import { InitOptions, close, flush, init } from "./init.mjs";
|
|
4
|
+
export { type CaptureErrorContext, type InitOptions, captureError, captureMessage, close, flush, init, withSpan };
|
package/dist/index.mjs
ADDED
package/dist/init.d.mts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
//#region src/init.d.ts
|
|
2
|
+
interface InitOptions {
|
|
3
|
+
/**
|
|
4
|
+
* Install a `process.on("uncaughtException")` listener that captures the
|
|
5
|
+
* error, flushes spans, and exits the process. Default `true`. Disable to
|
|
6
|
+
* use your own handler.
|
|
7
|
+
*/
|
|
8
|
+
readonly captureUncaughtException?: boolean;
|
|
9
|
+
/**
|
|
10
|
+
* Install a `process.on("unhandledRejection")` listener that captures the
|
|
11
|
+
* rejection and flushes spans. Default `true`. Disable to use your own
|
|
12
|
+
* handler. Whether the process exits is governed by Node's
|
|
13
|
+
* `--unhandled-rejections` flag, not the SDK.
|
|
14
|
+
*/
|
|
15
|
+
readonly captureUnhandledRejection?: boolean;
|
|
16
|
+
/**
|
|
17
|
+
* Sets the OTel `service.name` resource attribute — the identity spans
|
|
18
|
+
* are grouped by in the dashboard. Defaults to `OTEL_SERVICE_NAME` env
|
|
19
|
+
* var, then `"node-app"`. Use this to distinguish workers, crons, and
|
|
20
|
+
* other processes that share an `INTERFERE_API_KEY`.
|
|
21
|
+
*/
|
|
22
|
+
readonly serviceName?: string;
|
|
23
|
+
/**
|
|
24
|
+
* Sets the OTel `service.namespace` resource attribute. Optional; useful
|
|
25
|
+
* for grouping multiple services under one logical product surface
|
|
26
|
+
* (e.g. `"checkout"` across an API and worker fleet).
|
|
27
|
+
*/
|
|
28
|
+
readonly serviceNamespace?: string;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Bootstraps the Interfere Node SDK: wires a private `NodeTracerProvider`
|
|
32
|
+
* whose spans export to the Interfere collector via OTLP/HTTP, registers an
|
|
33
|
+
* `AsyncLocalStorageContextManager` so user spans propagate across async
|
|
34
|
+
* boundaries, and fetches the remote-config gate.
|
|
35
|
+
*
|
|
36
|
+
* Idempotent — safe to call from multiple entry points; only the first call
|
|
37
|
+
* wires up.
|
|
38
|
+
*
|
|
39
|
+
* Bails silently when `INTERFERE_API_KEY` is unset so dev runs without the
|
|
40
|
+
* SDK configured don't crash.
|
|
41
|
+
*
|
|
42
|
+
* The `release.slug` resource attribute is derived deterministically from
|
|
43
|
+
* the commit SHA (auto-detected from `INTERFERE_SOURCE_ID`,
|
|
44
|
+
* `VERCEL_GIT_COMMIT_SHA`, `GITHUB_SHA`, or `git rev-parse HEAD`) plus the
|
|
45
|
+
* API key — same algorithm used by `@interfere/next` and the browser SDKs,
|
|
46
|
+
* so spans from cohabiting frontends agree on release identity.
|
|
47
|
+
*/
|
|
48
|
+
declare function init(options?: InitOptions): Promise<void>;
|
|
49
|
+
/**
|
|
50
|
+
* Flushes any buffered spans and shuts down the tracer provider. Call from
|
|
51
|
+
* a server shutdown hook (`SIGTERM`, `process.on("beforeExit")`, framework
|
|
52
|
+
* lifecycle close) so in-flight error spans aren't lost on exit.
|
|
53
|
+
*/
|
|
54
|
+
declare function close(): Promise<void>;
|
|
55
|
+
/**
|
|
56
|
+
* Forces buffered spans to be exported immediately. Useful in serverless
|
|
57
|
+
* runtimes where the process is suspended between requests.
|
|
58
|
+
*/
|
|
59
|
+
declare function flush(): Promise<void>;
|
|
60
|
+
//#endregion
|
|
61
|
+
export { InitOptions, close, flush, init };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"init.d.mts","names":[],"sources":["../src/init.ts"],"mappings":";UAqBiB,WAAA;EAAA;;;;;EAAA,SAMN,wBAAA;EAcA;;;;AA2BX;;EA3BW,SAPA,yBAAA;EAkCmD;;;;;;EAAA,SA3BnD,WAAA;EA4GgB;;;;AAa3B;EAb2B,SAtGhB,gBAAA;AAAA;;;;;;;;;;;;;;;;;;;iBAqBW,IAAA,CAAK,OAAA,GAAS,WAAA,GAAmB,OAAA;;;;;;iBAiFjC,KAAA,CAAA,GAAS,OAAA;;;;;iBAaT,KAAA,CAAA,GAAS,OAAA"}
|
package/dist/init.mjs
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { readInterfereEnv } from "./internal/env.mjs";
|
|
2
|
+
import { fetchAndCacheRemoteConfig } from "./internal/remote-config.mjs";
|
|
3
|
+
import { installGlobalHandlers, uninstallGlobalHandlers } from "./internal/global-handlers.mjs";
|
|
4
|
+
import { resolveReleaseSlug } from "./internal/release-slug.mjs";
|
|
5
|
+
import { PRODUCER_VERSION } from "./internal/version.mjs";
|
|
6
|
+
import { API_PATHS } from "@interfere/constants/api";
|
|
7
|
+
import { AsyncLocalStorageContextManager } from "@opentelemetry/context-async-hooks";
|
|
8
|
+
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
|
|
9
|
+
import { resourceFromAttributes } from "@opentelemetry/resources";
|
|
10
|
+
import { BatchSpanProcessor } from "@opentelemetry/sdk-trace-base";
|
|
11
|
+
import { NodeTracerProvider } from "@opentelemetry/sdk-trace-node";
|
|
12
|
+
//#region src/init.ts
|
|
13
|
+
const DEFAULT_SERVICE_NAME = "node-app";
|
|
14
|
+
let provider = null;
|
|
15
|
+
/**
|
|
16
|
+
* Bootstraps the Interfere Node SDK: wires a private `NodeTracerProvider`
|
|
17
|
+
* whose spans export to the Interfere collector via OTLP/HTTP, registers an
|
|
18
|
+
* `AsyncLocalStorageContextManager` so user spans propagate across async
|
|
19
|
+
* boundaries, and fetches the remote-config gate.
|
|
20
|
+
*
|
|
21
|
+
* Idempotent — safe to call from multiple entry points; only the first call
|
|
22
|
+
* wires up.
|
|
23
|
+
*
|
|
24
|
+
* Bails silently when `INTERFERE_API_KEY` is unset so dev runs without the
|
|
25
|
+
* SDK configured don't crash.
|
|
26
|
+
*
|
|
27
|
+
* The `release.slug` resource attribute is derived deterministically from
|
|
28
|
+
* the commit SHA (auto-detected from `INTERFERE_SOURCE_ID`,
|
|
29
|
+
* `VERCEL_GIT_COMMIT_SHA`, `GITHUB_SHA`, or `git rev-parse HEAD`) plus the
|
|
30
|
+
* API key — same algorithm used by `@interfere/next` and the browser SDKs,
|
|
31
|
+
* so spans from cohabiting frontends agree on release identity.
|
|
32
|
+
*/
|
|
33
|
+
async function init(options = {}) {
|
|
34
|
+
if (provider) return;
|
|
35
|
+
const env = readInterfereEnv();
|
|
36
|
+
if (!env.apiKey) return;
|
|
37
|
+
const releaseSlug = env.forceEnable ? "rel_0000000000000000" : resolveReleaseSlug().slug;
|
|
38
|
+
if (!releaseSlug) console.warn("[interfere] No commit SHA available; server spans will ship without `release.slug`. Set `INTERFERE_SOURCE_ID` (or any of `VERCEL_GIT_COMMIT_SHA`, `GITHUB_SHA`) on the runtime env.");
|
|
39
|
+
provider = new NodeTracerProvider({
|
|
40
|
+
resource: resourceFromAttributes({
|
|
41
|
+
"service.name": options.serviceName ?? process.env["OTEL_SERVICE_NAME"] ?? DEFAULT_SERVICE_NAME,
|
|
42
|
+
...options.serviceNamespace ? { "service.namespace": options.serviceNamespace } : {},
|
|
43
|
+
"deployment.environment.name": env.nodeEnvironment ?? "unknown",
|
|
44
|
+
"telemetry.sdk.language": "nodejs",
|
|
45
|
+
"interfere.sdk.name": "@interfere/node",
|
|
46
|
+
"interfere.sdk.version": PRODUCER_VERSION,
|
|
47
|
+
...releaseSlug ? { "release.slug": releaseSlug } : {}
|
|
48
|
+
}),
|
|
49
|
+
spanProcessors: [new BatchSpanProcessor(new OTLPTraceExporter({
|
|
50
|
+
url: `${env.apiUrl}${API_PATHS.SINK}`,
|
|
51
|
+
headers: {
|
|
52
|
+
"user-agent": PRODUCER_VERSION,
|
|
53
|
+
"x-api-key": env.apiKey,
|
|
54
|
+
"x-interfere-producer-version": PRODUCER_VERSION,
|
|
55
|
+
...env.forceEnable ? { "x-interfere-force-enable": "1" } : {}
|
|
56
|
+
}
|
|
57
|
+
}))]
|
|
58
|
+
});
|
|
59
|
+
provider.register({ contextManager: new AsyncLocalStorageContextManager().enable() });
|
|
60
|
+
installGlobalHandlers({
|
|
61
|
+
captureUncaughtException: options.captureUncaughtException ?? true,
|
|
62
|
+
captureUnhandledRejection: options.captureUnhandledRejection ?? true,
|
|
63
|
+
flush
|
|
64
|
+
});
|
|
65
|
+
await fetchAndCacheRemoteConfig();
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Flushes any buffered spans and shuts down the tracer provider. Call from
|
|
69
|
+
* a server shutdown hook (`SIGTERM`, `process.on("beforeExit")`, framework
|
|
70
|
+
* lifecycle close) so in-flight error spans aren't lost on exit.
|
|
71
|
+
*/
|
|
72
|
+
async function close() {
|
|
73
|
+
if (!provider) return;
|
|
74
|
+
uninstallGlobalHandlers();
|
|
75
|
+
await provider.shutdown();
|
|
76
|
+
provider = null;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Forces buffered spans to be exported immediately. Useful in serverless
|
|
80
|
+
* runtimes where the process is suspended between requests.
|
|
81
|
+
*/
|
|
82
|
+
async function flush() {
|
|
83
|
+
if (!provider) return;
|
|
84
|
+
await provider.forceFlush();
|
|
85
|
+
}
|
|
86
|
+
//#endregion
|
|
87
|
+
export { close, flush, init };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"init.mjs","names":[],"sources":["../src/init.ts"],"sourcesContent":["import { API_PATHS } from \"@interfere/constants/api\";\n\nimport { AsyncLocalStorageContextManager } from \"@opentelemetry/context-async-hooks\";\nimport { OTLPTraceExporter } from \"@opentelemetry/exporter-trace-otlp-http\";\nimport { resourceFromAttributes } from \"@opentelemetry/resources\";\nimport { BatchSpanProcessor } from \"@opentelemetry/sdk-trace-base\";\nimport { NodeTracerProvider } from \"@opentelemetry/sdk-trace-node\";\n\nimport { readInterfereEnv } from \"./internal/env.js\";\nimport {\n installGlobalHandlers,\n uninstallGlobalHandlers,\n} from \"./internal/global-handlers.js\";\nimport { resolveReleaseSlug } from \"./internal/release-slug.js\";\nimport { fetchAndCacheRemoteConfig } from \"./internal/remote-config.js\";\nimport { PRODUCER_VERSION } from \"./internal/version.js\";\n\nconst DEFAULT_SERVICE_NAME = \"node-app\";\n\nlet provider: NodeTracerProvider | null = null;\n\nexport interface InitOptions {\n /**\n * Install a `process.on(\"uncaughtException\")` listener that captures the\n * error, flushes spans, and exits the process. Default `true`. Disable to\n * use your own handler.\n */\n readonly captureUncaughtException?: boolean;\n /**\n * Install a `process.on(\"unhandledRejection\")` listener that captures the\n * rejection and flushes spans. Default `true`. Disable to use your own\n * handler. Whether the process exits is governed by Node's\n * `--unhandled-rejections` flag, not the SDK.\n */\n readonly captureUnhandledRejection?: boolean;\n /**\n * Sets the OTel `service.name` resource attribute — the identity spans\n * are grouped by in the dashboard. Defaults to `OTEL_SERVICE_NAME` env\n * var, then `\"node-app\"`. Use this to distinguish workers, crons, and\n * other processes that share an `INTERFERE_API_KEY`.\n */\n readonly serviceName?: string;\n /**\n * Sets the OTel `service.namespace` resource attribute. Optional; useful\n * for grouping multiple services under one logical product surface\n * (e.g. `\"checkout\"` across an API and worker fleet).\n */\n readonly serviceNamespace?: string;\n}\n\n/**\n * Bootstraps the Interfere Node SDK: wires a private `NodeTracerProvider`\n * whose spans export to the Interfere collector via OTLP/HTTP, registers an\n * `AsyncLocalStorageContextManager` so user spans propagate across async\n * boundaries, and fetches the remote-config gate.\n *\n * Idempotent — safe to call from multiple entry points; only the first call\n * wires up.\n *\n * Bails silently when `INTERFERE_API_KEY` is unset so dev runs without the\n * SDK configured don't crash.\n *\n * The `release.slug` resource attribute is derived deterministically from\n * the commit SHA (auto-detected from `INTERFERE_SOURCE_ID`,\n * `VERCEL_GIT_COMMIT_SHA`, `GITHUB_SHA`, or `git rev-parse HEAD`) plus the\n * API key — same algorithm used by `@interfere/next` and the browser SDKs,\n * so spans from cohabiting frontends agree on release identity.\n */\nexport async function init(options: InitOptions = {}): Promise<void> {\n if (provider) {\n return;\n }\n\n const env = readInterfereEnv();\n if (!env.apiKey) {\n return;\n }\n\n // Force-enable swaps in the seeded `rel_local` slug so dev runs pass the\n // collector's preflight gate without uploading source maps. Prod\n // collectors drop force-enabled batches with a 2xx, so this header is\n // safe to ship in dev images that escape to prod by mistake. Same\n // pattern as `@interfere/next/config.ts` for `next dev`.\n const releaseSlug = env.forceEnable\n ? \"rel_0000000000000000\"\n : resolveReleaseSlug().slug;\n\n if (!releaseSlug) {\n console.warn(\n \"[interfere] No commit SHA available; server spans will ship without `release.slug`. Set `INTERFERE_SOURCE_ID` (or any of `VERCEL_GIT_COMMIT_SHA`, `GITHUB_SHA`) on the runtime env.\"\n );\n }\n\n const serviceName =\n options.serviceName ??\n process.env[\"OTEL_SERVICE_NAME\"] ??\n DEFAULT_SERVICE_NAME;\n\n const resource = resourceFromAttributes({\n \"service.name\": serviceName,\n ...(options.serviceNamespace\n ? { \"service.namespace\": options.serviceNamespace }\n : {}),\n \"deployment.environment.name\": env.nodeEnvironment ?? \"unknown\",\n \"telemetry.sdk.language\": \"nodejs\",\n \"interfere.sdk.name\": \"@interfere/node\",\n \"interfere.sdk.version\": PRODUCER_VERSION,\n ...(releaseSlug ? { \"release.slug\": releaseSlug } : {}),\n });\n\n const exporter = new OTLPTraceExporter({\n // Single opaque sink endpoint shared with browser/Next exporters; see\n // `@interfere/react/internal/otel/exporter` for the rationale.\n url: `${env.apiUrl}${API_PATHS.SINK}`,\n headers: {\n // The collector's bot filter (`v2/index.ts`) silently 202-drops\n // requests whose UA matches `isBot()`. Bun/Node's default UA gets\n // flagged, so SDKs self-identify; the bypass regex\n // `^@interfere/[a-z][a-z-]*@` lives in `collector/lib/bot-filter.ts`.\n \"user-agent\": PRODUCER_VERSION,\n \"x-api-key\": env.apiKey,\n \"x-interfere-producer-version\": PRODUCER_VERSION,\n ...(env.forceEnable ? { \"x-interfere-force-enable\": \"1\" } : {}),\n },\n });\n\n provider = new NodeTracerProvider({\n resource,\n spanProcessors: [new BatchSpanProcessor(exporter)],\n });\n\n provider.register({\n contextManager: new AsyncLocalStorageContextManager().enable(),\n });\n\n installGlobalHandlers({\n captureUncaughtException: options.captureUncaughtException ?? true,\n captureUnhandledRejection: options.captureUnhandledRejection ?? true,\n flush,\n });\n\n await fetchAndCacheRemoteConfig();\n}\n\n/**\n * Flushes any buffered spans and shuts down the tracer provider. Call from\n * a server shutdown hook (`SIGTERM`, `process.on(\"beforeExit\")`, framework\n * lifecycle close) so in-flight error spans aren't lost on exit.\n */\nexport async function close(): Promise<void> {\n if (!provider) {\n return;\n }\n uninstallGlobalHandlers();\n await provider.shutdown();\n provider = null;\n}\n\n/**\n * Forces buffered spans to be exported immediately. Useful in serverless\n * runtimes where the process is suspended between requests.\n */\nexport async function flush(): Promise<void> {\n if (!provider) {\n return;\n }\n await provider.forceFlush();\n}\n"],"mappings":";;;;;;;;;;;;AAiBA,MAAM,uBAAuB;AAE7B,IAAI,WAAsC;;;;;;;;;;;;;;;;;;;AAiD1C,eAAsB,KAAK,UAAuB,EAAE,EAAiB;CACnE,IAAI,UACF;CAGF,MAAM,MAAM,kBAAkB;CAC9B,IAAI,CAAC,IAAI,QACP;CAQF,MAAM,cAAc,IAAI,cACpB,yBACA,oBAAoB,CAAC;CAEzB,IAAI,CAAC,aACH,QAAQ,KACN,sLACD;CAoCH,WAAW,IAAI,mBAAmB;EAChC,UA7Be,uBAAuB;GACtC,gBALA,QAAQ,eACR,QAAQ,IAAI,wBACZ;GAIA,GAAI,QAAQ,mBACR,EAAE,qBAAqB,QAAQ,kBAAkB,GACjD,EAAE;GACN,+BAA+B,IAAI,mBAAmB;GACtD,0BAA0B;GAC1B,sBAAsB;GACtB,yBAAyB;GACzB,GAAI,cAAc,EAAE,gBAAgB,aAAa,GAAG,EAAE;GACvD,CAmBS;EACR,gBAAgB,CAAC,IAAI,mBAAmB,IAlBrB,kBAAkB;GAGrC,KAAK,GAAG,IAAI,SAAS,UAAU;GAC/B,SAAS;IAKP,cAAc;IACd,aAAa,IAAI;IACjB,gCAAgC;IAChC,GAAI,IAAI,cAAc,EAAE,4BAA4B,KAAK,GAAG,EAAE;IAC/D;GACF,CAIiD,CAAC,CAAC;EACnD,CAAC;CAEF,SAAS,SAAS,EAChB,gBAAgB,IAAI,iCAAiC,CAAC,QAAQ,EAC/D,CAAC;CAEF,sBAAsB;EACpB,0BAA0B,QAAQ,4BAA4B;EAC9D,2BAA2B,QAAQ,6BAA6B;EAChE;EACD,CAAC;CAEF,MAAM,2BAA2B;;;;;;;AAQnC,eAAsB,QAAuB;CAC3C,IAAI,CAAC,UACH;CAEF,yBAAyB;CACzB,MAAM,SAAS,UAAU;CACzB,WAAW;;;;;;AAOb,eAAsB,QAAuB;CAC3C,IAAI,CAAC,UACH;CAEF,MAAM,SAAS,YAAY"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dedupe.d.mts","names":[],"sources":["../../src/internal/dedupe.ts"],"mappings":";iBAEgB,eAAA,CAAgB,KAAA;AAAA,iBAIhB,iBAAA,CAAkB,KAAA"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
//#region src/internal/dedupe.ts
|
|
2
|
+
const seenErrors = /* @__PURE__ */ new WeakSet();
|
|
3
|
+
function isErrorCaptured(error) {
|
|
4
|
+
return error instanceof Error && seenErrors.has(error);
|
|
5
|
+
}
|
|
6
|
+
function markErrorCaptured(error) {
|
|
7
|
+
if (!(error instanceof Error)) return;
|
|
8
|
+
seenErrors.add(error);
|
|
9
|
+
}
|
|
10
|
+
//#endregion
|
|
11
|
+
export { isErrorCaptured, markErrorCaptured };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dedupe.mjs","names":[],"sources":["../../src/internal/dedupe.ts"],"sourcesContent":["const seenErrors = new WeakSet<Error>();\n\nexport function isErrorCaptured(error: unknown): boolean {\n return error instanceof Error && seenErrors.has(error);\n}\n\nexport function markErrorCaptured(error: unknown): void {\n if (!(error instanceof Error)) {\n return;\n }\n\n seenErrors.add(error);\n}\n"],"mappings":";AAAA,MAAM,6BAAa,IAAI,SAAgB;AAEvC,SAAgB,gBAAgB,OAAyB;CACvD,OAAO,iBAAiB,SAAS,WAAW,IAAI,MAAM;;AAGxD,SAAgB,kBAAkB,OAAsB;CACtD,IAAI,EAAE,iBAAiB,QACrB;CAGF,WAAW,IAAI,MAAM"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { Env } from "@interfere/types/sdk/runtime";
|
|
2
|
+
|
|
3
|
+
//#region src/internal/env.d.ts
|
|
4
|
+
interface InterfereEnv {
|
|
5
|
+
readonly apiKey: string | null;
|
|
6
|
+
readonly apiUrl: string;
|
|
7
|
+
readonly forceEnable: boolean;
|
|
8
|
+
readonly nodeEnvironment: Exclude<Env, null>;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Server SDKs always run end-to-end. There's no `NEXT_PUBLIC_*` force-enable
|
|
12
|
+
* escape hatch here — if `INTERFERE_API_KEY` is set, the SDK is on. The Next
|
|
13
|
+
* SDK guards on `NODE_ENV=production || NEXT_PUBLIC_INTERFERE_FORCE_ENABLE`
|
|
14
|
+
* because `next dev` runs the SDK during development without a real release
|
|
15
|
+
* slug; that asymmetry doesn't apply to plain Node servers.
|
|
16
|
+
*/
|
|
17
|
+
declare function isEnabledInEnvironment(): boolean;
|
|
18
|
+
declare function readInterfereEnv(): InterfereEnv;
|
|
19
|
+
//#endregion
|
|
20
|
+
export { InterfereEnv, isEnabledInEnvironment, readInterfereEnv };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"env.d.mts","names":[],"sources":["../../src/internal/env.ts"],"mappings":";;;UAKiB,YAAA;EAAA,SACN,MAAA;EAAA,SACA,MAAA;EAAA,SACA,WAAA;EAAA,SACA,eAAA,EAAiB,OAAA,CAAQ,GAAA;AAAA;;;;;;;;iBAUpB,sBAAA,CAAA;AAAA,iBAIA,gBAAA,CAAA,GAAoB,YAAA"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { API_URL } from "@interfere/constants/api";
|
|
2
|
+
import { parseEnvValue } from "@interfere/types/sdk/env";
|
|
3
|
+
import { normalizeEnv } from "@interfere/types/sdk/runtime";
|
|
4
|
+
//#region src/internal/env.ts
|
|
5
|
+
/**
|
|
6
|
+
* Server SDKs always run end-to-end. There's no `NEXT_PUBLIC_*` force-enable
|
|
7
|
+
* escape hatch here — if `INTERFERE_API_KEY` is set, the SDK is on. The Next
|
|
8
|
+
* SDK guards on `NODE_ENV=production || NEXT_PUBLIC_INTERFERE_FORCE_ENABLE`
|
|
9
|
+
* because `next dev` runs the SDK during development without a real release
|
|
10
|
+
* slug; that asymmetry doesn't apply to plain Node servers.
|
|
11
|
+
*/
|
|
12
|
+
function isEnabledInEnvironment() {
|
|
13
|
+
return true;
|
|
14
|
+
}
|
|
15
|
+
function readInterfereEnv() {
|
|
16
|
+
const nodeEnvironment = normalizeEnv(process.env["NODE_ENV"]) ?? "production";
|
|
17
|
+
return {
|
|
18
|
+
apiKey: parseEnvValue(process.env["INTERFERE_API_KEY"]),
|
|
19
|
+
apiUrl: parseEnvValue(process.env["INTERFERE_API_URL"]) ?? API_URL,
|
|
20
|
+
forceEnable: process.env["INTERFERE_FORCE_ENABLE"] === "1",
|
|
21
|
+
nodeEnvironment
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
//#endregion
|
|
25
|
+
export { isEnabledInEnvironment, readInterfereEnv };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"env.mjs","names":[],"sources":["../../src/internal/env.ts"],"sourcesContent":["import { API_URL } from \"@interfere/constants/api\";\nimport { parseEnvValue } from \"@interfere/types/sdk/env\";\nimport type { Env } from \"@interfere/types/sdk/runtime\";\nimport { normalizeEnv } from \"@interfere/types/sdk/runtime\";\n\nexport interface InterfereEnv {\n readonly apiKey: string | null;\n readonly apiUrl: string;\n readonly forceEnable: boolean;\n readonly nodeEnvironment: Exclude<Env, null>;\n}\n\n/**\n * Server SDKs always run end-to-end. There's no `NEXT_PUBLIC_*` force-enable\n * escape hatch here — if `INTERFERE_API_KEY` is set, the SDK is on. The Next\n * SDK guards on `NODE_ENV=production || NEXT_PUBLIC_INTERFERE_FORCE_ENABLE`\n * because `next dev` runs the SDK during development without a real release\n * slug; that asymmetry doesn't apply to plain Node servers.\n */\nexport function isEnabledInEnvironment(): boolean {\n return true;\n}\n\nexport function readInterfereEnv(): InterfereEnv {\n const nodeEnvironment = normalizeEnv(process.env[\"NODE_ENV\"]) ?? \"production\";\n\n return {\n apiKey: parseEnvValue(process.env[\"INTERFERE_API_KEY\"]),\n apiUrl: parseEnvValue(process.env[\"INTERFERE_API_URL\"]) ?? API_URL,\n forceEnable: process.env[\"INTERFERE_FORCE_ENABLE\"] === \"1\",\n nodeEnvironment,\n };\n}\n"],"mappings":";;;;;;;;;;;AAmBA,SAAgB,yBAAkC;CAChD,OAAO;;AAGT,SAAgB,mBAAiC;CAC/C,MAAM,kBAAkB,aAAa,QAAQ,IAAI,YAAY,IAAI;CAEjE,OAAO;EACL,QAAQ,cAAc,QAAQ,IAAI,qBAAqB;EACvD,QAAQ,cAAc,QAAQ,IAAI,qBAAqB,IAAI;EAC3D,aAAa,QAAQ,IAAI,8BAA8B;EACvD;EACD"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
//#region src/internal/global-handlers.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Installs `process.on("uncaughtException")` and
|
|
4
|
+
* `process.on("unhandledRejection")` listeners that report to Interfere.
|
|
5
|
+
*
|
|
6
|
+
* Uncaught exceptions: captured, spans flushed (best-effort, 2s timeout),
|
|
7
|
+
* then `process.exit(1)`. Once an `uncaughtException` listener is
|
|
8
|
+
* registered, Node will not exit on its own — leaving the process alive
|
|
9
|
+
* after an uncaught error usually means a corrupted state, so we own the
|
|
10
|
+
* exit. Customers who need different exit semantics should opt out and
|
|
11
|
+
* register their own handler.
|
|
12
|
+
*
|
|
13
|
+
* Unhandled rejections: captured and flushed, but the SDK does not call
|
|
14
|
+
* `process.exit`. Whether unhandled rejections are fatal is governed by
|
|
15
|
+
* Node's `--unhandled-rejections` flag (default `throw` since v15) and we
|
|
16
|
+
* don't override that.
|
|
17
|
+
*/
|
|
18
|
+
declare function installGlobalHandlers(options: {
|
|
19
|
+
readonly captureUncaughtException: boolean;
|
|
20
|
+
readonly captureUnhandledRejection: boolean;
|
|
21
|
+
readonly flush: () => Promise<void>;
|
|
22
|
+
}): void;
|
|
23
|
+
declare function uninstallGlobalHandlers(): void;
|
|
24
|
+
//#endregion
|
|
25
|
+
export { installGlobalHandlers, uninstallGlobalHandlers };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"global-handlers.d.mts","names":[],"sources":["../../src/internal/global-handlers.ts"],"mappings":";;AAyBA;;;;;;;;;;;AAoBA;;;;iBApBgB,qBAAA,CAAsB,OAAA;EAAA,SAC3B,wBAAA;EAAA,SACA,yBAAA;EAAA,SACA,KAAA,QAAa,OAAA;AAAA;AAAA,iBAiBR,uBAAA,CAAA"}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { captureError } from "../capture.mjs";
|
|
2
|
+
import { MECHANISM_TYPE } from "@interfere/types/sdk/errors";
|
|
3
|
+
//#region src/internal/global-handlers.ts
|
|
4
|
+
const FLUSH_TIMEOUT_MS = 2e3;
|
|
5
|
+
let uncaughtHandler = null;
|
|
6
|
+
let unhandledHandler = null;
|
|
7
|
+
/**
|
|
8
|
+
* Installs `process.on("uncaughtException")` and
|
|
9
|
+
* `process.on("unhandledRejection")` listeners that report to Interfere.
|
|
10
|
+
*
|
|
11
|
+
* Uncaught exceptions: captured, spans flushed (best-effort, 2s timeout),
|
|
12
|
+
* then `process.exit(1)`. Once an `uncaughtException` listener is
|
|
13
|
+
* registered, Node will not exit on its own — leaving the process alive
|
|
14
|
+
* after an uncaught error usually means a corrupted state, so we own the
|
|
15
|
+
* exit. Customers who need different exit semantics should opt out and
|
|
16
|
+
* register their own handler.
|
|
17
|
+
*
|
|
18
|
+
* Unhandled rejections: captured and flushed, but the SDK does not call
|
|
19
|
+
* `process.exit`. Whether unhandled rejections are fatal is governed by
|
|
20
|
+
* Node's `--unhandled-rejections` flag (default `throw` since v15) and we
|
|
21
|
+
* don't override that.
|
|
22
|
+
*/
|
|
23
|
+
function installGlobalHandlers(options) {
|
|
24
|
+
if (options.captureUncaughtException && uncaughtHandler === null) {
|
|
25
|
+
uncaughtHandler = (error) => {
|
|
26
|
+
handleUncaughtException(error, options.flush).catch(() => void 0);
|
|
27
|
+
};
|
|
28
|
+
process.on("uncaughtException", uncaughtHandler);
|
|
29
|
+
}
|
|
30
|
+
if (options.captureUnhandledRejection && unhandledHandler === null) {
|
|
31
|
+
unhandledHandler = (reason) => {
|
|
32
|
+
handleUnhandledRejection(reason, options.flush).catch(() => void 0);
|
|
33
|
+
};
|
|
34
|
+
process.on("unhandledRejection", unhandledHandler);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function uninstallGlobalHandlers() {
|
|
38
|
+
if (uncaughtHandler !== null) {
|
|
39
|
+
process.off("uncaughtException", uncaughtHandler);
|
|
40
|
+
uncaughtHandler = null;
|
|
41
|
+
}
|
|
42
|
+
if (unhandledHandler !== null) {
|
|
43
|
+
process.off("unhandledRejection", unhandledHandler);
|
|
44
|
+
unhandledHandler = null;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
async function handleUncaughtException(error, flush) {
|
|
48
|
+
await captureError(error, void 0, { mechanism: {
|
|
49
|
+
type: MECHANISM_TYPE.node.uncaughtException,
|
|
50
|
+
handled: false
|
|
51
|
+
} });
|
|
52
|
+
await flushWithTimeout(flush);
|
|
53
|
+
process.exit(1);
|
|
54
|
+
}
|
|
55
|
+
async function handleUnhandledRejection(reason, flush) {
|
|
56
|
+
await captureError(reason, void 0, { mechanism: {
|
|
57
|
+
type: MECHANISM_TYPE.node.unhandledRejection,
|
|
58
|
+
handled: false
|
|
59
|
+
} });
|
|
60
|
+
await flushWithTimeout(flush);
|
|
61
|
+
}
|
|
62
|
+
function flushWithTimeout(flush) {
|
|
63
|
+
return Promise.race([flush(), new Promise((resolve) => {
|
|
64
|
+
setTimeout(resolve, FLUSH_TIMEOUT_MS);
|
|
65
|
+
})]);
|
|
66
|
+
}
|
|
67
|
+
//#endregion
|
|
68
|
+
export { installGlobalHandlers, uninstallGlobalHandlers };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"global-handlers.mjs","names":[],"sources":["../../src/internal/global-handlers.ts"],"sourcesContent":["import { MECHANISM_TYPE } from \"@interfere/types/sdk/errors\";\n\nimport { captureError } from \"../capture.js\";\n\nconst FLUSH_TIMEOUT_MS = 2000;\n\nlet uncaughtHandler: NodeJS.UncaughtExceptionListener | null = null;\nlet unhandledHandler: NodeJS.UnhandledRejectionListener | null = null;\n\n/**\n * Installs `process.on(\"uncaughtException\")` and\n * `process.on(\"unhandledRejection\")` listeners that report to Interfere.\n *\n * Uncaught exceptions: captured, spans flushed (best-effort, 2s timeout),\n * then `process.exit(1)`. Once an `uncaughtException` listener is\n * registered, Node will not exit on its own — leaving the process alive\n * after an uncaught error usually means a corrupted state, so we own the\n * exit. Customers who need different exit semantics should opt out and\n * register their own handler.\n *\n * Unhandled rejections: captured and flushed, but the SDK does not call\n * `process.exit`. Whether unhandled rejections are fatal is governed by\n * Node's `--unhandled-rejections` flag (default `throw` since v15) and we\n * don't override that.\n */\nexport function installGlobalHandlers(options: {\n readonly captureUncaughtException: boolean;\n readonly captureUnhandledRejection: boolean;\n readonly flush: () => Promise<void>;\n}): void {\n if (options.captureUncaughtException && uncaughtHandler === null) {\n uncaughtHandler = (error) => {\n handleUncaughtException(error, options.flush).catch(() => undefined);\n };\n process.on(\"uncaughtException\", uncaughtHandler);\n }\n\n if (options.captureUnhandledRejection && unhandledHandler === null) {\n unhandledHandler = (reason) => {\n handleUnhandledRejection(reason, options.flush).catch(() => undefined);\n };\n process.on(\"unhandledRejection\", unhandledHandler);\n }\n}\n\nexport function uninstallGlobalHandlers(): void {\n if (uncaughtHandler !== null) {\n process.off(\"uncaughtException\", uncaughtHandler);\n uncaughtHandler = null;\n }\n if (unhandledHandler !== null) {\n process.off(\"unhandledRejection\", unhandledHandler);\n unhandledHandler = null;\n }\n}\n\nasync function handleUncaughtException(\n error: Error,\n flush: () => Promise<void>\n): Promise<void> {\n await captureError(error, undefined, {\n mechanism: {\n type: MECHANISM_TYPE.node.uncaughtException,\n handled: false,\n },\n });\n await flushWithTimeout(flush);\n process.exit(1);\n}\n\nasync function handleUnhandledRejection(\n reason: unknown,\n flush: () => Promise<void>\n): Promise<void> {\n await captureError(reason, undefined, {\n mechanism: {\n type: MECHANISM_TYPE.node.unhandledRejection,\n handled: false,\n },\n });\n await flushWithTimeout(flush);\n}\n\nfunction flushWithTimeout(flush: () => Promise<void>): Promise<void> {\n return Promise.race([\n flush(),\n new Promise<void>((resolve) => {\n setTimeout(resolve, FLUSH_TIMEOUT_MS);\n }),\n ]);\n}\n"],"mappings":";;;AAIA,MAAM,mBAAmB;AAEzB,IAAI,kBAA2D;AAC/D,IAAI,mBAA6D;;;;;;;;;;;;;;;;;AAkBjE,SAAgB,sBAAsB,SAI7B;CACP,IAAI,QAAQ,4BAA4B,oBAAoB,MAAM;EAChE,mBAAmB,UAAU;GAC3B,wBAAwB,OAAO,QAAQ,MAAM,CAAC,YAAY,KAAA,EAAU;;EAEtE,QAAQ,GAAG,qBAAqB,gBAAgB;;CAGlD,IAAI,QAAQ,6BAA6B,qBAAqB,MAAM;EAClE,oBAAoB,WAAW;GAC7B,yBAAyB,QAAQ,QAAQ,MAAM,CAAC,YAAY,KAAA,EAAU;;EAExE,QAAQ,GAAG,sBAAsB,iBAAiB;;;AAItD,SAAgB,0BAAgC;CAC9C,IAAI,oBAAoB,MAAM;EAC5B,QAAQ,IAAI,qBAAqB,gBAAgB;EACjD,kBAAkB;;CAEpB,IAAI,qBAAqB,MAAM;EAC7B,QAAQ,IAAI,sBAAsB,iBAAiB;EACnD,mBAAmB;;;AAIvB,eAAe,wBACb,OACA,OACe;CACf,MAAM,aAAa,OAAO,KAAA,GAAW,EACnC,WAAW;EACT,MAAM,eAAe,KAAK;EAC1B,SAAS;EACV,EACF,CAAC;CACF,MAAM,iBAAiB,MAAM;CAC7B,QAAQ,KAAK,EAAE;;AAGjB,eAAe,yBACb,QACA,OACe;CACf,MAAM,aAAa,QAAQ,KAAA,GAAW,EACpC,WAAW;EACT,MAAM,eAAe,KAAK;EAC1B,SAAS;EACV,EACF,CAAC;CACF,MAAM,iBAAiB,MAAM;;AAG/B,SAAS,iBAAiB,OAA2C;CACnE,OAAO,QAAQ,KAAK,CAClB,OAAO,EACP,IAAI,SAAe,YAAY;EAC7B,WAAW,SAAS,iBAAiB;GACrC,CACH,CAAC"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { NormalizedRequest } from "./types.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/internal/normalize-request.d.ts
|
|
4
|
+
declare const TRACEPARENT_HEADER = "traceparent";
|
|
5
|
+
/**
|
|
6
|
+
* Coerces a request-shaped value into the headers/method/path triple used
|
|
7
|
+
* by envelope construction. Handles WHATWG `Request`, Node
|
|
8
|
+
* `IncomingMessage`-like objects, and plain object literals.
|
|
9
|
+
*
|
|
10
|
+
* Returns `null` only for non-objects; otherwise produces a best-effort
|
|
11
|
+
* `NormalizedRequest` with sensible defaults.
|
|
12
|
+
*/
|
|
13
|
+
declare function normalizeRequest(request: unknown): NormalizedRequest | null;
|
|
14
|
+
//#endregion
|
|
15
|
+
export { TRACEPARENT_HEADER, normalizeRequest };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"normalize-request.d.mts","names":[],"sources":["../../src/internal/normalize-request.ts"],"mappings":";;;cAEa,kBAAA;;AAAb;;;;;AAUA;;iBAAgB,gBAAA,CAAiB,OAAA,YAAmB,iBAAA"}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
//#region src/internal/normalize-request.ts
|
|
2
|
+
const TRACEPARENT_HEADER = "traceparent";
|
|
3
|
+
/**
|
|
4
|
+
* Coerces a request-shaped value into the headers/method/path triple used
|
|
5
|
+
* by envelope construction. Handles WHATWG `Request`, Node
|
|
6
|
+
* `IncomingMessage`-like objects, and plain object literals.
|
|
7
|
+
*
|
|
8
|
+
* Returns `null` only for non-objects; otherwise produces a best-effort
|
|
9
|
+
* `NormalizedRequest` with sensible defaults.
|
|
10
|
+
*/
|
|
11
|
+
function normalizeRequest(request) {
|
|
12
|
+
if (request instanceof Request) return {
|
|
13
|
+
method: normalizeMethod(request.method),
|
|
14
|
+
path: normalizePath(request.url),
|
|
15
|
+
headers: request.headers
|
|
16
|
+
};
|
|
17
|
+
if (typeof request !== "object" || request === null) return null;
|
|
18
|
+
const value = request;
|
|
19
|
+
return {
|
|
20
|
+
method: normalizeMethod(value.method),
|
|
21
|
+
path: normalizePath(typeof value.path === "string" ? value.path : toStringOrDefault(value.url)),
|
|
22
|
+
headers: normalizeHeaders(value.headers)
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
function normalizeMethod(value) {
|
|
26
|
+
if (typeof value !== "string") return "GET";
|
|
27
|
+
const method = value.trim();
|
|
28
|
+
return method.length > 0 ? method.toUpperCase() : "GET";
|
|
29
|
+
}
|
|
30
|
+
function normalizePath(value) {
|
|
31
|
+
if (value.length === 0) return "/";
|
|
32
|
+
try {
|
|
33
|
+
return new URL(value, "http://localhost").pathname || "/";
|
|
34
|
+
} catch {
|
|
35
|
+
return value.startsWith("/") ? value : `/${value}`;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function toStringOrDefault(value, fallback = "/") {
|
|
39
|
+
if (typeof value !== "string") return fallback;
|
|
40
|
+
const trimmed = value.trim();
|
|
41
|
+
return trimmed.length > 0 ? trimmed : fallback;
|
|
42
|
+
}
|
|
43
|
+
function normalizeHeaders(value) {
|
|
44
|
+
if (value instanceof Headers) return new Headers(value);
|
|
45
|
+
if (typeof value !== "object" || value === null) return new Headers();
|
|
46
|
+
const headers = new Headers();
|
|
47
|
+
for (const [key, headerValue] of Object.entries(value)) {
|
|
48
|
+
if (headerValue === void 0) continue;
|
|
49
|
+
if (Array.isArray(headerValue)) {
|
|
50
|
+
headers.set(key, headerValue.join(", "));
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
headers.set(key, String(headerValue));
|
|
54
|
+
}
|
|
55
|
+
return headers;
|
|
56
|
+
}
|
|
57
|
+
//#endregion
|
|
58
|
+
export { TRACEPARENT_HEADER, normalizeRequest };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"normalize-request.mjs","names":[],"sources":["../../src/internal/normalize-request.ts"],"sourcesContent":["import type { NormalizedRequest } from \"./types.js\";\n\nexport const TRACEPARENT_HEADER = \"traceparent\";\n\n/**\n * Coerces a request-shaped value into the headers/method/path triple used\n * by envelope construction. Handles WHATWG `Request`, Node\n * `IncomingMessage`-like objects, and plain object literals.\n *\n * Returns `null` only for non-objects; otherwise produces a best-effort\n * `NormalizedRequest` with sensible defaults.\n */\nexport function normalizeRequest(request: unknown): NormalizedRequest | null {\n if (request instanceof Request) {\n return {\n method: normalizeMethod(request.method),\n path: normalizePath(request.url),\n headers: request.headers,\n };\n }\n\n if (typeof request !== \"object\" || request === null) {\n return null;\n }\n\n const value = request as {\n headers?: unknown;\n method?: unknown;\n path?: unknown;\n url?: unknown;\n };\n\n return {\n method: normalizeMethod(value.method),\n path: normalizePath(\n typeof value.path === \"string\" ? value.path : toStringOrDefault(value.url)\n ),\n headers: normalizeHeaders(value.headers),\n };\n}\n\nfunction normalizeMethod(value: unknown): string {\n if (typeof value !== \"string\") {\n return \"GET\";\n }\n\n const method = value.trim();\n return method.length > 0 ? method.toUpperCase() : \"GET\";\n}\n\nfunction normalizePath(value: string): string {\n if (value.length === 0) {\n return \"/\";\n }\n\n try {\n const parsed = new URL(value, \"http://localhost\");\n return parsed.pathname || \"/\";\n } catch {\n return value.startsWith(\"/\") ? value : `/${value}`;\n }\n}\n\nfunction toStringOrDefault(value: unknown, fallback = \"/\"): string {\n if (typeof value !== \"string\") {\n return fallback;\n }\n\n const trimmed = value.trim();\n return trimmed.length > 0 ? trimmed : fallback;\n}\n\nfunction normalizeHeaders(value: unknown): Headers {\n if (value instanceof Headers) {\n return new Headers(value);\n }\n\n if (typeof value !== \"object\" || value === null) {\n return new Headers();\n }\n\n const headers = new Headers();\n for (const [key, headerValue] of Object.entries(value)) {\n if (headerValue === undefined) {\n continue;\n }\n\n if (Array.isArray(headerValue)) {\n headers.set(key, headerValue.join(\", \"));\n continue;\n }\n\n headers.set(key, String(headerValue));\n }\n\n return headers;\n}\n"],"mappings":";AAEA,MAAa,qBAAqB;;;;;;;;;AAUlC,SAAgB,iBAAiB,SAA4C;CAC3E,IAAI,mBAAmB,SACrB,OAAO;EACL,QAAQ,gBAAgB,QAAQ,OAAO;EACvC,MAAM,cAAc,QAAQ,IAAI;EAChC,SAAS,QAAQ;EAClB;CAGH,IAAI,OAAO,YAAY,YAAY,YAAY,MAC7C,OAAO;CAGT,MAAM,QAAQ;CAOd,OAAO;EACL,QAAQ,gBAAgB,MAAM,OAAO;EACrC,MAAM,cACJ,OAAO,MAAM,SAAS,WAAW,MAAM,OAAO,kBAAkB,MAAM,IAAI,CAC3E;EACD,SAAS,iBAAiB,MAAM,QAAQ;EACzC;;AAGH,SAAS,gBAAgB,OAAwB;CAC/C,IAAI,OAAO,UAAU,UACnB,OAAO;CAGT,MAAM,SAAS,MAAM,MAAM;CAC3B,OAAO,OAAO,SAAS,IAAI,OAAO,aAAa,GAAG;;AAGpD,SAAS,cAAc,OAAuB;CAC5C,IAAI,MAAM,WAAW,GACnB,OAAO;CAGT,IAAI;EAEF,OAAO,IADY,IAAI,OAAO,mBACjB,CAAC,YAAY;SACpB;EACN,OAAO,MAAM,WAAW,IAAI,GAAG,QAAQ,IAAI;;;AAI/C,SAAS,kBAAkB,OAAgB,WAAW,KAAa;CACjE,IAAI,OAAO,UAAU,UACnB,OAAO;CAGT,MAAM,UAAU,MAAM,MAAM;CAC5B,OAAO,QAAQ,SAAS,IAAI,UAAU;;AAGxC,SAAS,iBAAiB,OAAyB;CACjD,IAAI,iBAAiB,SACnB,OAAO,IAAI,QAAQ,MAAM;CAG3B,IAAI,OAAO,UAAU,YAAY,UAAU,MACzC,OAAO,IAAI,SAAS;CAGtB,MAAM,UAAU,IAAI,SAAS;CAC7B,KAAK,MAAM,CAAC,KAAK,gBAAgB,OAAO,QAAQ,MAAM,EAAE;EACtD,IAAI,gBAAgB,KAAA,GAClB;EAGF,IAAI,MAAM,QAAQ,YAAY,EAAE;GAC9B,QAAQ,IAAI,KAAK,YAAY,KAAK,KAAK,CAAC;GACxC;;EAGF,QAAQ,IAAI,KAAK,OAAO,YAAY,CAAC;;CAGvC,OAAO"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"release-slug.d.mts","names":[],"sources":["../../src/internal/release-slug.ts"],"mappings":";iBAkBgB,gBAAA,CAAA;AAAA,iBAOA,kBAAA,CAAA;EACd,SAAA;EACA,IAAA;AAAA"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { readFirstEnvValue } from "@interfere/types/sdk/env";
|
|
2
|
+
import { releaseSourceIdEnvKeys } from "@interfere/types/integrations";
|
|
3
|
+
import { deriveReleaseSlug } from "@interfere/types/releases/slug";
|
|
4
|
+
import { execSync } from "node:child_process";
|
|
5
|
+
//#region src/internal/release-slug.ts
|
|
6
|
+
function runGitCommand(command) {
|
|
7
|
+
try {
|
|
8
|
+
const output = execSync(command, {
|
|
9
|
+
encoding: "utf8",
|
|
10
|
+
stdio: [
|
|
11
|
+
"ignore",
|
|
12
|
+
"pipe",
|
|
13
|
+
"ignore"
|
|
14
|
+
]
|
|
15
|
+
}).trim();
|
|
16
|
+
return output.length > 0 ? output : null;
|
|
17
|
+
} catch {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function resolveCommitSha() {
|
|
22
|
+
return readFirstEnvValue(process.env, releaseSourceIdEnvKeys) ?? runGitCommand("git rev-parse HEAD");
|
|
23
|
+
}
|
|
24
|
+
function resolveReleaseSlug() {
|
|
25
|
+
const commitSha = resolveCommitSha();
|
|
26
|
+
return {
|
|
27
|
+
commitSha,
|
|
28
|
+
slug: commitSha ? deriveReleaseSlug(commitSha) : null
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
//#endregion
|
|
32
|
+
export { resolveCommitSha, resolveReleaseSlug };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"release-slug.mjs","names":[],"sources":["../../src/internal/release-slug.ts"],"sourcesContent":["import { releaseSourceIdEnvKeys } from \"@interfere/types/integrations\";\nimport { deriveReleaseSlug } from \"@interfere/types/releases/slug\";\nimport { readFirstEnvValue } from \"@interfere/types/sdk/env\";\n\nimport { execSync } from \"node:child_process\";\n\nfunction runGitCommand(command: string): string | null {\n try {\n const output = execSync(command, {\n encoding: \"utf8\",\n stdio: [\"ignore\", \"pipe\", \"ignore\"],\n }).trim();\n return output.length > 0 ? output : null;\n } catch {\n return null;\n }\n}\n\nexport function resolveCommitSha(): string | null {\n return (\n readFirstEnvValue(process.env, releaseSourceIdEnvKeys) ??\n runGitCommand(\"git rev-parse HEAD\")\n );\n}\n\nexport function resolveReleaseSlug(): {\n commitSha: string | null;\n slug: string | null;\n} {\n const commitSha = resolveCommitSha();\n return {\n commitSha,\n slug: commitSha ? deriveReleaseSlug(commitSha) : null,\n };\n}\n"],"mappings":";;;;;AAMA,SAAS,cAAc,SAAgC;CACrD,IAAI;EACF,MAAM,SAAS,SAAS,SAAS;GAC/B,UAAU;GACV,OAAO;IAAC;IAAU;IAAQ;IAAS;GACpC,CAAC,CAAC,MAAM;EACT,OAAO,OAAO,SAAS,IAAI,SAAS;SAC9B;EACN,OAAO;;;AAIX,SAAgB,mBAAkC;CAChD,OACE,kBAAkB,QAAQ,KAAK,uBAAuB,IACtD,cAAc,qBAAqB;;AAIvC,SAAgB,qBAGd;CACA,MAAM,YAAY,kBAAkB;CACpC,OAAO;EACL;EACA,MAAM,YAAY,kBAAkB,UAAU,GAAG;EAClD"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"remote-config.d.mts","names":[],"sources":["../../src/internal/remote-config.ts"],"mappings":";iBAUsB,yBAAA,CAAA,GAA6B,OAAA;AAAA,iBAkCnC,eAAA,CAAgB,MAAA"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { isEnabledInEnvironment, readInterfereEnv } from "./env.mjs";
|
|
2
|
+
import { API_PATHS } from "@interfere/constants/api";
|
|
3
|
+
//#region src/internal/remote-config.ts
|
|
4
|
+
let cachedConfig = null;
|
|
5
|
+
async function fetchAndCacheRemoteConfig() {
|
|
6
|
+
if (!isEnabledInEnvironment()) return;
|
|
7
|
+
const env = readInterfereEnv();
|
|
8
|
+
if (env.apiKey === null) return;
|
|
9
|
+
try {
|
|
10
|
+
const url = `${env.apiUrl}${API_PATHS.CONFIG}`;
|
|
11
|
+
const response = await fetch(url, {
|
|
12
|
+
method: "GET",
|
|
13
|
+
headers: {
|
|
14
|
+
"content-type": "application/json",
|
|
15
|
+
"x-api-key": env.apiKey
|
|
16
|
+
},
|
|
17
|
+
signal: AbortSignal.timeout(1e4)
|
|
18
|
+
});
|
|
19
|
+
if (!response.ok) return;
|
|
20
|
+
const config = await response.json();
|
|
21
|
+
if (config?.plugins) cachedConfig = config.plugins;
|
|
22
|
+
} catch {}
|
|
23
|
+
}
|
|
24
|
+
function isPluginEnabled(plugin) {
|
|
25
|
+
if (!cachedConfig) return true;
|
|
26
|
+
return cachedConfig[plugin] !== false;
|
|
27
|
+
}
|
|
28
|
+
//#endregion
|
|
29
|
+
export { fetchAndCacheRemoteConfig, isPluginEnabled };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"remote-config.mjs","names":[],"sources":["../../src/internal/remote-config.ts"],"sourcesContent":["import { API_PATHS } from \"@interfere/constants/api\";\nimport type {\n RemoteConfig,\n RemotePluginConfig,\n} from \"@interfere/types/sdk/remote-config\";\n\nimport { isEnabledInEnvironment, readInterfereEnv } from \"./env.js\";\n\nlet cachedConfig: RemotePluginConfig | null = null;\n\nexport async function fetchAndCacheRemoteConfig(): Promise<void> {\n if (!isEnabledInEnvironment()) {\n return;\n }\n\n const env = readInterfereEnv();\n if (env.apiKey === null) {\n return;\n }\n\n try {\n const url = `${env.apiUrl}${API_PATHS.CONFIG}`;\n const response = await fetch(url, {\n method: \"GET\",\n headers: {\n \"content-type\": \"application/json\",\n \"x-api-key\": env.apiKey,\n },\n signal: AbortSignal.timeout(10_000),\n });\n\n if (!response.ok) {\n return;\n }\n\n const config = (await response.json()) as RemoteConfig;\n if (config?.plugins) {\n cachedConfig = config.plugins;\n }\n } catch {\n // Fail silently — all plugins remain enabled\n }\n}\n\nexport function isPluginEnabled(plugin: string): boolean {\n if (!cachedConfig) {\n return true;\n }\n return cachedConfig[plugin as keyof RemotePluginConfig] !== false;\n}\n"],"mappings":";;;AAQA,IAAI,eAA0C;AAE9C,eAAsB,4BAA2C;CAC/D,IAAI,CAAC,wBAAwB,EAC3B;CAGF,MAAM,MAAM,kBAAkB;CAC9B,IAAI,IAAI,WAAW,MACjB;CAGF,IAAI;EACF,MAAM,MAAM,GAAG,IAAI,SAAS,UAAU;EACtC,MAAM,WAAW,MAAM,MAAM,KAAK;GAChC,QAAQ;GACR,SAAS;IACP,gBAAgB;IAChB,aAAa,IAAI;IAClB;GACD,QAAQ,YAAY,QAAQ,IAAO;GACpC,CAAC;EAEF,IAAI,CAAC,SAAS,IACZ;EAGF,MAAM,SAAU,MAAM,SAAS,MAAM;EACrC,IAAI,QAAQ,SACV,eAAe,OAAO;SAElB;;AAKV,SAAgB,gBAAgB,QAAyB;CACvD,IAAI,CAAC,cACH,OAAO;CAET,OAAO,aAAa,YAAwC"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { NodeContext } from "@interfere/types/sdk/plugins/context/node";
|
|
2
|
+
import { ErrorMechanism } from "@interfere/types/sdk/plugins/payload/errors";
|
|
3
|
+
|
|
4
|
+
//#region src/internal/types.d.ts
|
|
5
|
+
interface CaptureErrorContext {
|
|
6
|
+
readonly mechanism?: ErrorMechanism;
|
|
7
|
+
readonly node?: Omit<NodeContext, "runtime">;
|
|
8
|
+
readonly traceparent?: string;
|
|
9
|
+
}
|
|
10
|
+
interface NormalizedRequest {
|
|
11
|
+
readonly headers: Headers;
|
|
12
|
+
readonly method: string;
|
|
13
|
+
readonly path: string;
|
|
14
|
+
}
|
|
15
|
+
//#endregion
|
|
16
|
+
export { CaptureErrorContext, NormalizedRequest };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.mts","names":[],"sources":["../../src/internal/types.ts"],"mappings":";;;;UAGiB,mBAAA;EAAA,SACN,SAAA,GAAY,cAAA;EAAA,SACZ,IAAA,GAAO,IAAA,CAAK,WAAA;EAAA,SACZ,WAAA;AAAA;AAAA,UAGM,iBAAA;EAAA,SACN,OAAA,EAAS,OAAA;EAAA,SACT,MAAA;EAAA,SACA,IAAA;AAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"version.d.mts","names":[],"sources":["../../src/internal/version.ts"],"mappings":";cAEa,gBAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"version.mjs","names":["pkg.name","pkg.version"],"sources":["../../src/internal/version.ts"],"sourcesContent":["import pkg from \"../../package.json\" with { type: \"json\" };\n\nexport const PRODUCER_VERSION = `${pkg.name}@${pkg.version}`;\n"],"mappings":";;AAEA,MAAa,mBAAmB,GAAGA,KAAS,GAAGC"}
|
package/dist/package.mjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"package.mjs","names":[],"sources":["../package.json"],"sourcesContent":[""],"mappings":""}
|
package/package.json
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@interfere/node",
|
|
3
|
+
"version": "0.0.1-canary.0",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"description": "Node.js SDK for Interfere. Error tracking and OTel instrumentation for any Node server.",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"observability",
|
|
8
|
+
"typescript",
|
|
9
|
+
"nodejs",
|
|
10
|
+
"error-tracking",
|
|
11
|
+
"opentelemetry"
|
|
12
|
+
],
|
|
13
|
+
"homepage": "https://interfere.com",
|
|
14
|
+
"bugs": {
|
|
15
|
+
"url": "mailto:support@interfere.com"
|
|
16
|
+
},
|
|
17
|
+
"author": "Interfere <support@interfere.com> (https://interfere.com)",
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "git+https://github.com/interfere-inc/interfere.git",
|
|
21
|
+
"directory": "src/packages/public/node"
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
"dist"
|
|
25
|
+
],
|
|
26
|
+
"type": "module",
|
|
27
|
+
"exports": {
|
|
28
|
+
".": {
|
|
29
|
+
"@source": "./src/index.ts",
|
|
30
|
+
"types": "./dist/index.d.mts",
|
|
31
|
+
"default": "./dist/index.mjs"
|
|
32
|
+
},
|
|
33
|
+
"./init": {
|
|
34
|
+
"@source": "./src/init.ts",
|
|
35
|
+
"types": "./dist/init.d.mts",
|
|
36
|
+
"default": "./dist/init.mjs"
|
|
37
|
+
},
|
|
38
|
+
"./capture": {
|
|
39
|
+
"@source": "./src/capture.ts",
|
|
40
|
+
"types": "./dist/capture.d.mts",
|
|
41
|
+
"default": "./dist/capture.mjs"
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
"sideEffects": false,
|
|
45
|
+
"publishConfig": {
|
|
46
|
+
"access": "public",
|
|
47
|
+
"tag": "canary"
|
|
48
|
+
},
|
|
49
|
+
"scripts": {
|
|
50
|
+
"build": "tsdown",
|
|
51
|
+
"test": "vitest run --passWithNoTests",
|
|
52
|
+
"typecheck": "tsc --noEmit --incremental"
|
|
53
|
+
},
|
|
54
|
+
"dependencies": {
|
|
55
|
+
"@interfere/constants": "^9.0.1",
|
|
56
|
+
"@interfere/types": "^9.0.0",
|
|
57
|
+
"@opentelemetry/api": "^1.9.1",
|
|
58
|
+
"@opentelemetry/context-async-hooks": "^2.7.1",
|
|
59
|
+
"@opentelemetry/exporter-trace-otlp-http": "^0.217.0",
|
|
60
|
+
"@opentelemetry/resources": "^2.7.0",
|
|
61
|
+
"@opentelemetry/sdk-trace-base": "^2.7.0",
|
|
62
|
+
"@opentelemetry/sdk-trace-node": "^2.7.1",
|
|
63
|
+
"uuid": "^14.0.0"
|
|
64
|
+
},
|
|
65
|
+
"devDependencies": {
|
|
66
|
+
"@interfere/test-utils": "^9.0.0",
|
|
67
|
+
"@interfere/typescript-config": "^9.0.0",
|
|
68
|
+
"@types/node": "^24.12.0",
|
|
69
|
+
"@vitest/coverage-v8": "^4.1.6",
|
|
70
|
+
"tsdown": "^0.22.0",
|
|
71
|
+
"typescript": "^6.0.3",
|
|
72
|
+
"vitest": "^4.1.6"
|
|
73
|
+
}
|
|
74
|
+
}
|