@foam-ai/node 0.1.0-alpha.3 → 0.1.0-alpha.5

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.
Files changed (56) hide show
  1. package/README.md +538 -75
  2. package/dist/constants.d.ts +43 -0
  3. package/dist/constants.js +97 -0
  4. package/dist/endpoint.d.ts +2 -0
  5. package/dist/endpoint.js +11 -0
  6. package/dist/exporters.d.ts +25 -0
  7. package/dist/exporters.js +103 -0
  8. package/dist/index.d.ts +7 -0
  9. package/dist/index.js +26 -0
  10. package/dist/ingest.d.ts +6 -0
  11. package/dist/ingest.js +61 -0
  12. package/dist/init.d.ts +22 -0
  13. package/dist/init.js +150 -0
  14. package/dist/instrumentations.d.ts +3 -0
  15. package/dist/instrumentations.js +101 -0
  16. package/dist/logs.d.ts +9 -0
  17. package/dist/logs.js +61 -0
  18. package/dist/metrics.d.ts +5 -0
  19. package/dist/metrics.js +29 -0
  20. package/dist/network-capture/collector.d.ts +27 -0
  21. package/dist/network-capture/collector.js +356 -0
  22. package/dist/network-capture/http.d.ts +5 -0
  23. package/dist/network-capture/http.js +221 -0
  24. package/dist/network-capture/index.d.ts +2 -0
  25. package/dist/network-capture/index.js +17 -0
  26. package/dist/network-capture/redact.d.ts +6 -0
  27. package/dist/network-capture/redact.js +87 -0
  28. package/dist/network-capture/undici.d.ts +5 -0
  29. package/dist/network-capture/undici.js +177 -0
  30. package/dist/otlp.d.ts +8 -0
  31. package/dist/otlp.js +46 -0
  32. package/dist/propagation.d.ts +5 -0
  33. package/dist/propagation.js +45 -0
  34. package/dist/report.d.ts +9 -0
  35. package/dist/report.js +56 -0
  36. package/dist/resource.d.ts +3 -0
  37. package/dist/resource.js +24 -0
  38. package/dist/state.d.ts +26 -0
  39. package/dist/state.js +61 -0
  40. package/dist/traces.d.ts +1 -0
  41. package/dist/traces.js +21 -0
  42. package/dist/utils.d.ts +4 -0
  43. package/dist/utils.js +20 -0
  44. package/package.json +47 -19
  45. package/dist/node/src/capture-exception.d.ts +0 -9
  46. package/dist/node/src/capture-exception.js +0 -31
  47. package/dist/node/src/index.d.ts +0 -13
  48. package/dist/node/src/index.js +0 -19
  49. package/dist/node/src/init.d.ts +0 -27
  50. package/dist/node/src/init.js +0 -218
  51. package/dist/node/src/metrics.d.ts +0 -28
  52. package/dist/node/src/metrics.js +0 -69
  53. package/dist/shared/constants.d.ts +0 -1
  54. package/dist/shared/constants.js +0 -4
  55. package/dist/shared/util.d.ts +0 -9
  56. package/dist/shared/util.js +0 -21
package/dist/report.js ADDED
@@ -0,0 +1,56 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.report = report;
4
+ const api_1 = require("@opentelemetry/api");
5
+ const api_logs_1 = require("@opentelemetry/api-logs");
6
+ const core_1 = require("@opentelemetry/core");
7
+ const semantic_conventions_1 = require("@opentelemetry/semantic-conventions");
8
+ const constants_js_1 = require("./constants.js");
9
+ const otlp_js_1 = require("./otlp.js");
10
+ const state_js_1 = require("./state.js");
11
+ const logger = api_1.diag.createComponentLogger({ namespace: constants_js_1.FOAM_IDENTIFIER_NAME });
12
+ // pcga11: Guarded so repeat report() calls don't trigger diag's "logger will be overwritten" warning.
13
+ let reportingConfigured = false;
14
+ function ensureReportingConfigured() {
15
+ if (reportingConfigured) {
16
+ return;
17
+ }
18
+ reportingConfigured = true;
19
+ const fromEnv = (0, core_1.diagLogLevelFromString)((0, core_1.getStringFromEnv)(constants_js_1.DIAG_LOG_LEVEL));
20
+ api_1.diag.setLogger(new api_1.DiagConsoleLogger(), fromEnv ?? api_1.DiagLogLevel.INFO);
21
+ }
22
+ // pcga11: State reports go to Foam's OTLP endpoint directly instead of through the
23
+ // logger provider or exporter, since those may themselves be broken or unregistered.
24
+ async function report({ name, environment, token, severity, message, error, }) {
25
+ ensureReportingConfigured();
26
+ // pcga11: Local diag logging happens before the token check so misconfiguration
27
+ // is visible in the console even when nothing can be sent.
28
+ if (severity >= api_logs_1.SeverityNumber.ERROR) {
29
+ logger.error(message);
30
+ }
31
+ else if (severity >= api_logs_1.SeverityNumber.WARN) {
32
+ logger.warn(message);
33
+ }
34
+ else {
35
+ logger.info(message);
36
+ }
37
+ // pcga11: Token is the only hard requirement (nothing can be sent without auth).
38
+ // name/environment fall back to "unknown" so misconfiguration reports still arrive.
39
+ if (!token) {
40
+ return;
41
+ }
42
+ await (0, otlp_js_1.sendOtlpLog)({
43
+ token: token,
44
+ resourceAttributes: {
45
+ [semantic_conventions_1.ATTR_SERVICE_NAME]: name?.trim() || "unknown",
46
+ [semantic_conventions_1.ATTR_DEPLOYMENT_ENVIRONMENT_NAME]: environment?.trim() || "unknown",
47
+ },
48
+ scopeName: constants_js_1.FOAM_IDENTIFIER_NAME,
49
+ severityNumber: severity,
50
+ body: JSON.stringify({
51
+ state: { ...(0, state_js_1.getState)() },
52
+ message: `${constants_js_1.FOAM_IDENTIFIER_NAME} ${message}`,
53
+ error: error,
54
+ }),
55
+ });
56
+ }
@@ -0,0 +1,3 @@
1
+ import type { Attributes } from "@opentelemetry/api";
2
+ import { type Resource } from "@opentelemetry/resources";
3
+ export declare function createFoamResource(name: string, environment: string, version?: string, additionalResourceAttributes?: Attributes): Resource;
@@ -0,0 +1,24 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createFoamResource = createFoamResource;
4
+ const resources_1 = require("@opentelemetry/resources");
5
+ const semantic_conventions_1 = require("@opentelemetry/semantic-conventions");
6
+ const constants_js_1 = require("./constants.js");
7
+ function createFoamResource(name, environment, version, additionalResourceAttributes) {
8
+ return (0, resources_1.defaultResource)()
9
+ .merge((0, resources_1.detectResources)({
10
+ detectors: [resources_1.envDetector, resources_1.processDetector, resources_1.hostDetector, resources_1.osDetector],
11
+ }))
12
+ // pcga11: Later merges overwrite colliding keys, so Foam name/env/version will win over detectors.
13
+ // This is the desired behavior. Do not change.
14
+ .merge((0, resources_1.resourceFromAttributes)({
15
+ ...additionalResourceAttributes,
16
+ [semantic_conventions_1.ATTR_SERVICE_NAME]: name,
17
+ [semantic_conventions_1.ATTR_DEPLOYMENT_ENVIRONMENT_NAME]: environment,
18
+ ...(version
19
+ ? { [semantic_conventions_1.ATTR_SERVICE_VERSION]: version }
20
+ : {}),
21
+ [semantic_conventions_1.ATTR_TELEMETRY_DISTRO_NAME]: constants_js_1.FOAM_DISTRO_NAME,
22
+ [semantic_conventions_1.ATTR_TELEMETRY_DISTRO_VERSION]: constants_js_1.FOAM_DISTRO_VERSION,
23
+ }));
24
+ }
@@ -0,0 +1,26 @@
1
+ export declare enum Signals {
2
+ traces = "traces",
3
+ metrics = "metrics",
4
+ logs = "logs",
5
+ baggage = "baggage",
6
+ profile = "profile"
7
+ }
8
+ export declare enum SignalSources {
9
+ none = "none",
10
+ global = "global",
11
+ ingest = "ingest",
12
+ local = "local"
13
+ }
14
+ export interface State {
15
+ readonly initialized: boolean;
16
+ readonly instrumentations: readonly string[];
17
+ readonly signals: Readonly<Record<Signals, SignalSources>>;
18
+ readonly params: Readonly<Record<string, unknown>>;
19
+ }
20
+ export declare const setInstrumentations: (names: readonly string[]) => void;
21
+ export declare const setSignal: (signal: Signals, source: SignalSources) => void;
22
+ export declare const getSignal: (signal: Signals) => SignalSources;
23
+ export declare const setInitialized: (initialized: boolean) => void;
24
+ export declare const getInitialized: () => boolean;
25
+ export declare const setParams: (params: Record<string, unknown>) => void;
26
+ export declare const getState: () => State;
package/dist/state.js ADDED
@@ -0,0 +1,61 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getState = exports.setParams = exports.getInitialized = exports.setInitialized = exports.getSignal = exports.setSignal = exports.setInstrumentations = exports.SignalSources = exports.Signals = void 0;
4
+ var Signals;
5
+ (function (Signals) {
6
+ Signals["traces"] = "traces";
7
+ Signals["metrics"] = "metrics";
8
+ Signals["logs"] = "logs";
9
+ Signals["baggage"] = "baggage";
10
+ Signals["profile"] = "profile";
11
+ })(Signals || (exports.Signals = Signals = {}));
12
+ var SignalSources;
13
+ (function (SignalSources) {
14
+ SignalSources["none"] = "none";
15
+ SignalSources["global"] = "global";
16
+ SignalSources["ingest"] = "ingest";
17
+ SignalSources["local"] = "local";
18
+ })(SignalSources || (exports.SignalSources = SignalSources = {}));
19
+ const state = {
20
+ initialized: false,
21
+ instrumentations: [], // pcga11: The registered instrumentations that are enabled in the current application.
22
+ signals: {
23
+ traces: SignalSources.none,
24
+ metrics: SignalSources.none,
25
+ logs: SignalSources.none,
26
+ baggage: SignalSources.none,
27
+ profile: SignalSources.none,
28
+ },
29
+ params: {},
30
+ };
31
+ const setInstrumentations = (names) => {
32
+ state.instrumentations = [...names];
33
+ };
34
+ exports.setInstrumentations = setInstrumentations;
35
+ const setSignal = (signal, source) => {
36
+ state.signals[signal] = source;
37
+ };
38
+ exports.setSignal = setSignal;
39
+ const getSignal = (signal) => state.signals[signal];
40
+ exports.getSignal = getSignal;
41
+ const setInitialized = (initialized) => {
42
+ state.initialized = initialized;
43
+ };
44
+ exports.setInitialized = setInitialized;
45
+ const getInitialized = () => state.initialized;
46
+ exports.getInitialized = getInitialized;
47
+ const setParams = (params) => {
48
+ // pcga11: Safety precaution: never store tokens in the state.
49
+ if (params.token) {
50
+ delete params.token;
51
+ }
52
+ state.params = { ...params };
53
+ };
54
+ exports.setParams = setParams;
55
+ const getState = () => ({
56
+ initialized: state.initialized,
57
+ instrumentations: [...state.instrumentations],
58
+ signals: { ...state.signals },
59
+ params: { ...state.params },
60
+ });
61
+ exports.getState = getState;
@@ -0,0 +1 @@
1
+ export declare function recordException(error: unknown): void;
package/dist/traces.js ADDED
@@ -0,0 +1,21 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.recordException = recordException;
4
+ const api_1 = require("@opentelemetry/api");
5
+ const utils_js_1 = require("./utils.js");
6
+ // pcga11: Not gated on the traces signal on purpose: the active span belongs to
7
+ // whichever SDK owns tracing, and the exception should land on that span either way.
8
+ function recordException(error) {
9
+ (0, utils_js_1.safely)(() => {
10
+ const span = api_1.trace.getActiveSpan();
11
+ if (!span?.isRecording()) {
12
+ return;
13
+ }
14
+ const exception = error instanceof Error ? error : String(error);
15
+ span.recordException(exception);
16
+ span.setStatus({
17
+ code: api_1.SpanStatusCode.ERROR,
18
+ message: error instanceof Error ? error.message : String(error),
19
+ });
20
+ });
21
+ }
@@ -0,0 +1,4 @@
1
+ import { Signals } from "./state.js";
2
+ export declare function safely<T>(operation: () => T, fallback: T): T;
3
+ export declare function safely(operation: () => void): void;
4
+ export declare function whenInitialized<A extends unknown[]>(signal: Signals, operation: (...args: A) => void): (...args: A) => void;
package/dist/utils.js ADDED
@@ -0,0 +1,20 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.safely = safely;
4
+ exports.whenInitialized = whenInitialized;
5
+ const state_js_1 = require("./state.js");
6
+ function safely(operation, fallback) {
7
+ try {
8
+ return operation();
9
+ }
10
+ catch {
11
+ return fallback;
12
+ }
13
+ }
14
+ function whenInitialized(signal, operation) {
15
+ return (...args) => {
16
+ if ((0, state_js_1.getSignal)(signal) === state_js_1.SignalSources.none)
17
+ return;
18
+ safely(() => operation(...args));
19
+ };
20
+ }
package/package.json CHANGED
@@ -1,35 +1,63 @@
1
1
  {
2
2
  "name": "@foam-ai/node",
3
- "version": "0.1.0-alpha.3",
4
- "main": "dist/node/src/index.js",
5
- "types": "dist/node/src/index.d.ts",
3
+ "version": "0.1.0-alpha.5",
4
+ "description": "Foam JavaScript Node.js SDK",
5
+ "license": "Apache-2.0",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/foam-ai/js-node.git"
9
+ },
10
+ "publishConfig": {
11
+ "access": "public",
12
+ "tag": "alpha"
13
+ },
14
+ "engines": {
15
+ "node": "^18.19.0 || >=20.6.0"
16
+ },
17
+ "main": "./dist/index.js",
18
+ "module": "./dist/index.js",
19
+ "types": "./dist/index.d.ts",
6
20
  "exports": {
7
21
  ".": {
8
- "types": "./dist/node/src/index.d.ts",
9
- "import": "./dist/node/src/index.js",
10
- "require": "./dist/node/src/index.js"
22
+ "types": "./dist/index.d.ts",
23
+ "import": "./dist/index.js",
24
+ "require": "./dist/index.js",
25
+ "default": "./dist/index.js"
11
26
  }
12
27
  },
13
28
  "files": [
14
29
  "dist"
15
30
  ],
16
31
  "scripts": {
17
- "build": "tsc -p tsconfig.json"
32
+ "build": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\" && tsc -p tsconfig.build.json",
33
+ "typecheck": "tsc --noEmit",
34
+ "test": "vitest run",
35
+ "prepublishOnly": "npm run build"
18
36
  },
19
37
  "dependencies": {
20
- "@opentelemetry/api": "^1.9.0",
21
- "@opentelemetry/api-logs": "^0.214.0",
22
- "@opentelemetry/exporter-logs-otlp-http": "^0.214.0",
23
- "@opentelemetry/exporter-metrics-otlp-http": "^0.214.0",
24
- "@opentelemetry/exporter-trace-otlp-http": "^0.214.0",
25
- "@opentelemetry/instrumentation": "^0.214.0",
26
- "@opentelemetry/resources": "^2.6.1",
27
- "@opentelemetry/sdk-logs": "^0.214.0",
28
- "@opentelemetry/sdk-metrics": "^2.6.1",
29
- "@opentelemetry/sdk-trace-base": "^2.6.1",
30
- "@opentelemetry/sdk-trace-node": "^2.6.1"
38
+ "@opentelemetry/api": "^1.9.1",
39
+ "@opentelemetry/api-logs": "^0.221.0",
40
+ "@opentelemetry/auto-instrumentations-node": "^0.79.0",
41
+ "@opentelemetry/context-async-hooks": "^2.10.0",
42
+ "@opentelemetry/core": "^2.10.0",
43
+ "@opentelemetry/exporter-logs-otlp-http": "^0.221.0",
44
+ "@opentelemetry/exporter-metrics-otlp-http": "^0.221.0",
45
+ "@opentelemetry/exporter-trace-otlp-http": "^0.221.0",
46
+ "@opentelemetry/instrumentation": "^0.221.0",
47
+ "@opentelemetry/instrumentation-console": "^0.3.0",
48
+ "@opentelemetry/instrumentation-http": "^0.221.0",
49
+ "@opentelemetry/instrumentation-undici": "^0.31.0",
50
+ "@opentelemetry/otlp-transformer": "^0.221.0",
51
+ "@opentelemetry/resources": "^2.10.0",
52
+ "@opentelemetry/sdk-logs": "^0.221.0",
53
+ "@opentelemetry/sdk-metrics": "^2.10.0",
54
+ "@opentelemetry/sdk-trace-base": "^2.10.0",
55
+ "@opentelemetry/semantic-conventions": "^1.43.0",
56
+ "@opentelemetry/winston-transport": "^0.31.0"
31
57
  },
32
58
  "devDependencies": {
33
- "typescript": "^6.0.2"
59
+ "@types/node": "^26.2.0",
60
+ "typescript": "^7.0.2",
61
+ "vitest": "^4.1.11"
34
62
  }
35
63
  }
@@ -1,9 +0,0 @@
1
- /**
2
- * Captures an exception and sends it to Foam.
3
- *
4
- * - Records on the active span if one exists (for trace-level visibility)
5
- * - Always emits an OTEL log record so the error reaches Foam even when
6
- * there is no active span or auto-instrumentation
7
- * - Safe to call at any time — silently no-ops if the SDK isn't initialized
8
- */
9
- export declare const captureException: (error: unknown) => void;
@@ -1,31 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.captureException = void 0;
4
- const api_1 = require("@opentelemetry/api");
5
- const api_logs_1 = require("@opentelemetry/api-logs");
6
- const util_1 = require("../../shared/util");
7
- /**
8
- * Captures an exception and sends it to Foam.
9
- *
10
- * - Records on the active span if one exists (for trace-level visibility)
11
- * - Always emits an OTEL log record so the error reaches Foam even when
12
- * there is no active span or auto-instrumentation
13
- * - Safe to call at any time — silently no-ops if the SDK isn't initialized
14
- */
15
- exports.captureException = (0, util_1.safe)((error) => {
16
- const err = error instanceof Error ? error : new Error(String(error));
17
- const span = api_1.trace.getActiveSpan();
18
- if (span) {
19
- span.recordException(err);
20
- span.setStatus({ code: api_1.SpanStatusCode.ERROR });
21
- }
22
- api_logs_1.logs.getLogger('foam').emit({
23
- severityNumber: api_logs_1.SeverityNumber.ERROR,
24
- severityText: 'ERROR',
25
- body: err.message,
26
- attributes: {
27
- 'exception.type': err.name,
28
- 'exception.stacktrace': err.stack ?? '',
29
- },
30
- });
31
- });
@@ -1,13 +0,0 @@
1
- import { captureException } from './capture-exception';
2
- import { init } from './init';
3
- import { incrementCounter, recordHistogram, recordGauge } from './metrics';
4
- export { captureException, init, incrementCounter, recordHistogram, recordGauge };
5
- export type { FoamNodeInitOptions } from './init';
6
- declare const foam: {
7
- captureException: (error: unknown) => void;
8
- init: (options: import("./init").FoamNodeInitOptions) => void;
9
- incrementCounter: (name: string, value?: number, attributes?: import("@opentelemetry/api").Attributes) => void;
10
- recordHistogram: (name: string, value: number, attributes?: import("@opentelemetry/api").Attributes) => void;
11
- recordGauge: (name: string, value: number, attributes?: import("@opentelemetry/api").Attributes) => void;
12
- };
13
- export default foam;
@@ -1,19 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.recordGauge = exports.recordHistogram = exports.incrementCounter = exports.init = exports.captureException = void 0;
4
- const capture_exception_1 = require("./capture-exception");
5
- Object.defineProperty(exports, "captureException", { enumerable: true, get: function () { return capture_exception_1.captureException; } });
6
- const init_1 = require("./init");
7
- Object.defineProperty(exports, "init", { enumerable: true, get: function () { return init_1.init; } });
8
- const metrics_1 = require("./metrics");
9
- Object.defineProperty(exports, "incrementCounter", { enumerable: true, get: function () { return metrics_1.incrementCounter; } });
10
- Object.defineProperty(exports, "recordHistogram", { enumerable: true, get: function () { return metrics_1.recordHistogram; } });
11
- Object.defineProperty(exports, "recordGauge", { enumerable: true, get: function () { return metrics_1.recordGauge; } });
12
- const foam = {
13
- captureException: capture_exception_1.captureException,
14
- init: init_1.init,
15
- incrementCounter: metrics_1.incrementCounter,
16
- recordHistogram: metrics_1.recordHistogram,
17
- recordGauge: metrics_1.recordGauge,
18
- };
19
- exports.default = foam;
@@ -1,27 +0,0 @@
1
- import type { InstrumentationBase } from '@opentelemetry/instrumentation';
2
- export interface FoamNodeInitOptions {
3
- apiKey: string;
4
- serviceName: string;
5
- isProduction: boolean;
6
- /**
7
- * OpenTelemetry instrumentations to register (e.g. getNodeAutoInstrumentations()).
8
- * None are registered by default to keep the SDK webpack/Turbopack-safe.
9
- * For plain Node.js servers, pass getNodeAutoInstrumentations() to get
10
- * automatic HTTP, Express, DNS, etc. instrumentation.
11
- */
12
- instrumentations?: InstrumentationBase[];
13
- }
14
- /**
15
- * Initializes the Foam SDK for Node.js.
16
- *
17
- * - Production-only: silently no-ops when `isProduction` is false.
18
- * - Zero latency: all setup is synchronous object construction; actual telemetry
19
- * export happens asynchronously in background batch intervals.
20
- * - Crash-safe: every signal is wrapped in its own try/catch so a failure in
21
- * one (e.g. traces) never prevents the others (logs, metrics) from starting.
22
- * - Provider-aware: detects each OTEL signal independently. If another SDK
23
- * (Sentry, @vercel/otel, etc.) already registered a provider, Foam attaches
24
- * its exporters on top. If no provider exists, Foam creates one. This
25
- * guarantees all three signals always reach Foam regardless of environment.
26
- */
27
- export declare const init: (options: FoamNodeInitOptions) => void;
@@ -1,218 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.init = void 0;
4
- const sdk_trace_node_1 = require("@opentelemetry/sdk-trace-node");
5
- const sdk_trace_base_1 = require("@opentelemetry/sdk-trace-base");
6
- const exporter_trace_otlp_http_1 = require("@opentelemetry/exporter-trace-otlp-http");
7
- const exporter_logs_otlp_http_1 = require("@opentelemetry/exporter-logs-otlp-http");
8
- const sdk_logs_1 = require("@opentelemetry/sdk-logs");
9
- const sdk_metrics_1 = require("@opentelemetry/sdk-metrics");
10
- const exporter_metrics_otlp_http_1 = require("@opentelemetry/exporter-metrics-otlp-http");
11
- const instrumentation_1 = require("@opentelemetry/instrumentation");
12
- const api_1 = require("@opentelemetry/api");
13
- const api_logs_1 = require("@opentelemetry/api-logs");
14
- const resources_1 = require("@opentelemetry/resources");
15
- const util_1 = require("util");
16
- const constants_1 = require("../../shared/constants");
17
- const util_2 = require("../../shared/util");
18
- const capture_exception_1 = require("./capture-exception");
19
- /**
20
- * Initializes the Foam SDK for Node.js.
21
- *
22
- * - Production-only: silently no-ops when `isProduction` is false.
23
- * - Zero latency: all setup is synchronous object construction; actual telemetry
24
- * export happens asynchronously in background batch intervals.
25
- * - Crash-safe: every signal is wrapped in its own try/catch so a failure in
26
- * one (e.g. traces) never prevents the others (logs, metrics) from starting.
27
- * - Provider-aware: detects each OTEL signal independently. If another SDK
28
- * (Sentry, @vercel/otel, etc.) already registered a provider, Foam attaches
29
- * its exporters on top. If no provider exists, Foam creates one. This
30
- * guarantees all three signals always reach Foam regardless of environment.
31
- */
32
- exports.init = (0, util_2.safe)((options) => {
33
- if (!options.isProduction) {
34
- return;
35
- }
36
- const headers = { authorization: `Bearer ${options.apiKey}` };
37
- const resource = (0, resources_1.resourceFromAttributes)({ 'service.name': options.serviceName });
38
- // Each signal is independent — a failure in one must not block the others
39
- safeRun(() => ensureTraces(headers, resource, options.instrumentations));
40
- safeRun(() => ensureLogs(headers, resource));
41
- safeRun(() => ensureMetrics(headers, resource));
42
- safeRun(() => patchConsole());
43
- safeRun(() => registerProcessErrorHandlers());
44
- });
45
- // ---------------------------------------------------------------------------
46
- // Per-signal setup
47
- //
48
- // For each signal the pattern is:
49
- // 1. Create a batch processor/reader with an HTTP OTLP exporter
50
- // 2. Check if a real provider is already registered globally
51
- // 3. If yes → attach the processor to the existing provider
52
- // 4. If no → create a new provider and register it globally
53
- //
54
- // All exporters use HTTP (not gRPC) to stay webpack/Turbopack-compatible.
55
- // Batch processors buffer spans/logs in memory and flush asynchronously,
56
- // so init() returns instantly with zero latency impact on the caller.
57
- // ---------------------------------------------------------------------------
58
- function ensureTraces(headers, resource, instrumentations) {
59
- const processor = new sdk_trace_base_1.BatchSpanProcessor(new exporter_trace_otlp_http_1.OTLPTraceExporter({ url: `${constants_1.FOAM_OTEL_ENDPOINT}/v1/traces`, headers }));
60
- const existing = unwrapProvider(api_1.trace.getTracerProvider());
61
- if (!addProcessor(existing, 'addSpanProcessor', '_activeSpanProcessor', processor)) {
62
- new sdk_trace_node_1.NodeTracerProvider({ resource, spanProcessors: [processor] }).register();
63
- }
64
- if (instrumentations?.length) {
65
- (0, instrumentation_1.registerInstrumentations)({ instrumentations });
66
- }
67
- }
68
- function ensureLogs(headers, resource) {
69
- const processor = new sdk_logs_1.BatchLogRecordProcessor(new exporter_logs_otlp_http_1.OTLPLogExporter({ url: `${constants_1.FOAM_OTEL_ENDPOINT}/v1/logs`, headers }));
70
- const existing = unwrapProvider(api_logs_1.logs.getLoggerProvider());
71
- if (!addProcessor(existing, 'addLogRecordProcessor', '_sharedState', processor)) {
72
- const provider = new sdk_logs_1.LoggerProvider({ resource, processors: [processor] });
73
- api_logs_1.logs.setGlobalLoggerProvider(provider);
74
- }
75
- }
76
- function ensureMetrics(headers, resource) {
77
- const reader = new sdk_metrics_1.PeriodicExportingMetricReader({
78
- exporter: new exporter_metrics_otlp_http_1.OTLPMetricExporter({
79
- url: `${constants_1.FOAM_OTEL_ENDPOINT}/v1/metrics`,
80
- headers,
81
- }),
82
- });
83
- const existing = unwrapProvider(api_1.metrics.getMeterProvider());
84
- if (!addProcessor(existing, 'addMetricReader', '_sharedState', reader)) {
85
- const provider = new sdk_metrics_1.MeterProvider({ resource, readers: [reader] });
86
- api_1.metrics.setGlobalMeterProvider(provider);
87
- }
88
- }
89
- // ---------------------------------------------------------------------------
90
- // Helpers
91
- // ---------------------------------------------------------------------------
92
- /** Runs a function, swallowing any error so one failed signal can't block others. */
93
- function safeRun(fn) {
94
- try {
95
- fn();
96
- }
97
- catch { /* intentionally swallowed */ }
98
- }
99
- /**
100
- * Unwraps proxy/noop providers to get the real delegate.
101
- *
102
- * OTEL wraps every global provider in a Proxy (ProxyTracerProvider,
103
- * ProxyLoggerProvider, etc.). Before checking for methods like
104
- * addSpanProcessor we need the real underlying provider.
105
- *
106
- * Returns {} for noop providers so callers can safely check for methods
107
- * with typeof without null guards.
108
- */
109
- function unwrapProvider(provider) {
110
- const delegate = typeof provider.getDelegate === 'function'
111
- ? provider.getDelegate()
112
- : provider._delegate ?? provider;
113
- if (!delegate || delegate.constructor?.name?.startsWith('Noop'))
114
- return {};
115
- return delegate;
116
- }
117
- /**
118
- * Attempts to add a processor/reader to an existing provider.
119
- *
120
- * Strategy:
121
- * 1. Try the public v1 method (addSpanProcessor, addLogRecordProcessor, addMetricReader)
122
- * 2. Fall back to pushing into internal arrays used by SDK v2:
123
- * - Traces: provider._activeSpanProcessor._spanProcessors
124
- * - Logs: provider._sharedState.registeredLogRecordProcessors
125
- * - Metrics: provider._sharedState has no writable reader array, so metrics
126
- * always creates a new provider if the public method is missing
127
- *
128
- * All internal access is guarded by Array.isArray — safe if internals change.
129
- * Returns true if successfully attached, false if a new provider is needed.
130
- */
131
- function addProcessor(provider, v1Method, v2Field, processor) {
132
- if (typeof provider[v1Method] === 'function') {
133
- provider[v1Method](processor);
134
- return true;
135
- }
136
- const state = provider[v2Field];
137
- if (!state)
138
- return false;
139
- const internal = state._spanProcessors ??
140
- state._processors ??
141
- state.registeredLogRecordProcessors;
142
- if (Array.isArray(internal)) {
143
- internal.push(processor);
144
- return true;
145
- }
146
- return false;
147
- }
148
- // ---------------------------------------------------------------------------
149
- // Uncaught error handlers
150
- //
151
- // Listens to process-level uncaughtException and unhandledRejection events
152
- // to capture errors that escape all try/catch blocks. Works for any framework
153
- // (Express, Fastify, Koa, Next.js, plain Node.js, background jobs, etc.).
154
- // Idempotent — safe to call multiple times. Never interferes with the app's
155
- // default crash behavior since we only listen (not override).
156
- // ---------------------------------------------------------------------------
157
- const PROCESS_HANDLERS_INSTALLED = Symbol.for('foam.process.handlers');
158
- function registerProcessErrorHandlers() {
159
- if (globalThis[PROCESS_HANDLERS_INSTALLED])
160
- return;
161
- process.on('uncaughtException', (err) => {
162
- try {
163
- (0, capture_exception_1.captureException)(err);
164
- }
165
- catch { /* never interfere with the crash */ }
166
- });
167
- process.on('unhandledRejection', (reason) => {
168
- try {
169
- (0, capture_exception_1.captureException)(reason);
170
- }
171
- catch { /* never interfere with the crash */ }
172
- });
173
- globalThis[PROCESS_HANDLERS_INSTALLED] = true;
174
- }
175
- // ---------------------------------------------------------------------------
176
- // Console log capture
177
- //
178
- // Wraps console.log/info/error/warn/debug to emit OTel log records.
179
- // Uses a Symbol to ensure idempotency — safe to call multiple times
180
- // or if another SDK has already patched console.
181
- // The original console method is always called first so there's zero
182
- // impact on the application's logging behavior or latency.
183
- // ---------------------------------------------------------------------------
184
- const PATCHED = Symbol.for('foam.console.patched');
185
- function patchConsole() {
186
- if (console[PATCHED])
187
- return;
188
- const logger = api_logs_1.logs.getLogger('console');
189
- const patch = (method, severityNumber, severityText) => {
190
- const original = console[method].bind(console);
191
- console[method] = (...args) => {
192
- original(...args);
193
- let ctx;
194
- let traceAttributes;
195
- try {
196
- ctx = api_1.context.active();
197
- const spanContext = api_1.trace.getSpanContext(ctx);
198
- if (spanContext && (0, api_1.isSpanContextValid)(spanContext)) {
199
- traceAttributes = { 'trace.id': spanContext.traceId, 'span.id': spanContext.spanId };
200
- }
201
- }
202
- catch { /* never break the caller's console.log */ }
203
- logger.emit({
204
- context: ctx,
205
- severityNumber,
206
- severityText,
207
- body: (0, util_1.format)(...args),
208
- attributes: traceAttributes,
209
- });
210
- };
211
- };
212
- patch('log', api_logs_1.SeverityNumber.INFO, 'INFO');
213
- patch('info', api_logs_1.SeverityNumber.INFO, 'INFO');
214
- patch('error', api_logs_1.SeverityNumber.ERROR, 'ERROR');
215
- patch('warn', api_logs_1.SeverityNumber.WARN, 'WARN');
216
- patch('debug', api_logs_1.SeverityNumber.DEBUG, 'DEBUG');
217
- console[PATCHED] = true;
218
- }