@heystack/otel 0.0.1 → 0.2.1
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 +87 -3
- package/dist/core.d.ts +17 -0
- package/dist/core.js +6 -0
- package/dist/next.d.ts +21 -0
- package/dist/next.js +45 -0
- package/dist/node.d.ts +12 -0
- package/dist/node.js +30 -0
- package/dist/workers.d.ts +159 -0
- package/dist/workers.js +338 -0
- package/package.json +15 -6
- package/dist/index.d.ts +0 -22
- package/dist/index.js +0 -21
package/README.md
CHANGED
|
@@ -1,15 +1,99 @@
|
|
|
1
1
|
# @heystack/otel
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Runtime-aware OpenTelemetry tracing that exports to Heystack. One import per runtime, so the `.` entry stays safe to load anywhere (including Edge/Workers) without dragging in the Node SDK.
|
|
4
4
|
|
|
5
5
|
```bash
|
|
6
6
|
npm install @heystack/otel
|
|
7
7
|
```
|
|
8
8
|
|
|
9
|
+
Always read your key from the environment — never paste it into source:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
# .env
|
|
13
|
+
HEYSTACK_API_KEY=sk_live_…
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
> **Requires `@heystack/otel` `>=0.2.0`.** See [Migration](#migration--versioning) below.
|
|
17
|
+
|
|
18
|
+
## Runtime matrix
|
|
19
|
+
|
|
20
|
+
| Runtime | Import | Notes |
|
|
21
|
+
| --- | --- | --- |
|
|
22
|
+
| Next.js — **any** deploy target (Vercel/Node **and** Cloudflare/OpenNext) | `@heystack/otel/next` | `registerHeystack` in `instrumentation.ts`. Auto-detects Node vs Cloudflare workerd and picks the right exporter. No-op on Edge. |
|
|
23
|
+
| Standalone Cloudflare Workers (hand-written `export default { fetch }`) | `@heystack/otel/workers` | `instrument()` wraps your handler. Fetch-based exporter, flushes via `ctx.waitUntil`. |
|
|
24
|
+
| Node / Express / Fastify / NestJS (long-running server) | `@heystack/otel/node` | `initHeystack`: auto-instrumentations + graceful shutdown. |
|
|
25
|
+
| Anywhere (pure helpers) | `@heystack/otel` | `buildExporterConfig`, types. No Node SDK loaded. |
|
|
26
|
+
|
|
27
|
+
## Node / Express / etc.
|
|
28
|
+
|
|
9
29
|
At the very top of your app's entry file:
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
import { initHeystack } from "@heystack/otel/node";
|
|
33
|
+
|
|
34
|
+
initHeystack({ apiKey: process.env.HEYSTACK_API_KEY, service: "my-app" });
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
This enables auto-instrumentations (HTTP, Express, etc.) so you get spans without manual wiring.
|
|
38
|
+
|
|
39
|
+
## Next.js (any deploy target, including Cloudflare/OpenNext)
|
|
40
|
+
|
|
41
|
+
In `instrumentation.ts` at the project root:
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
export async function register() {
|
|
45
|
+
const { registerHeystack } = await import("@heystack/otel/next");
|
|
46
|
+
await registerHeystack({ service: "my-app" });
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
`apiKey` defaults to `process.env.HEYSTACK_API_KEY`. `registerHeystack` is **runtime-aware** and safe to call unconditionally:
|
|
51
|
+
|
|
52
|
+
- **Vercel / Node deploys** → uses the Node OTel SDK (auto-instrumentations).
|
|
53
|
+
- **Cloudflare / OpenNext (workerd)** → Next still reports `NEXT_RUNTIME === "nodejs"`, but the code actually runs on workerd where the Node SDK's `node:http` OTLP exporter can't send (it initializes and silently exports nothing). `registerHeystack` detects workerd and registers the **fetch-based** exporter instead, so framework spans actually ship. No separate setup or import needed.
|
|
54
|
+
- **Edge runtime** → no-op (neither SDK can run there).
|
|
55
|
+
|
|
56
|
+
## Standalone Cloudflare Workers
|
|
57
|
+
|
|
58
|
+
For a hand-written Worker (`export default { fetch }`), wrap the handler with `instrument()`:
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
import { instrument } from "@heystack/otel/workers";
|
|
62
|
+
|
|
63
|
+
export default instrument(
|
|
64
|
+
{
|
|
65
|
+
async fetch(req, env, ctx) {
|
|
66
|
+
return new Response("ok");
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
{ service: "my-worker" }, // apiKey defaults to env.HEYSTACK_API_KEY
|
|
70
|
+
);
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
`instrument()` must be the **outermost** wrapper if other middleware also wraps the handler, so the request span covers everything inside:
|
|
74
|
+
|
|
10
75
|
```ts
|
|
11
|
-
|
|
12
|
-
initHeystack({ apiKey: process.env.HEYSTACK_KEY!, service: "my-app" });
|
|
76
|
+
export default instrument(withOtherMiddleware(worker), { service: "my-worker" });
|
|
13
77
|
```
|
|
14
78
|
|
|
79
|
+
Set the key as a secret: `wrangler secret put HEYSTACK_API_KEY`.
|
|
80
|
+
|
|
81
|
+
## Flushing
|
|
82
|
+
|
|
83
|
+
On Workers/edge the export is a `fetch()` POST, and the isolate can be torn down the instant your handler returns. **You must let that POST complete or the trace is silently dropped** — this is the #1 cause of flaky Workers tracing. `flushHeystack()` and `instrument()`'s built-in flush both await the in-flight fetch (not just the OTel span processor, which does *not* wait for it).
|
|
84
|
+
|
|
85
|
+
- **Standalone Workers (`instrument()`)** — flushes automatically. After the response it `ctx.waitUntil`s a promise that drains both the span processor and the exporter's in-flight fetch, so the POST finishes before the isolate is killed. No action needed.
|
|
86
|
+
- **Next on workerd (`registerHeystack`)** — there's no per-request `ExecutionContext`, so spans are **not** auto-flushed. For guaranteed delivery, `import { flushHeystack } from "@heystack/otel/workers"` and call it from a response hook (or `ctx.waitUntil(flushHeystack())` if you have a ctx) after handling a request. `flushHeystack()` awaits the export fetch.
|
|
87
|
+
- **Node (`initHeystack`)** — flushes on `SIGTERM`/`SIGINT` automatically.
|
|
88
|
+
|
|
89
|
+
## Migration / versioning
|
|
90
|
+
|
|
91
|
+
- **Pin `@heystack/otel` `>=0.2.0`** — the workerd-aware `/next` path and `initHeystackWorkers` / `flushHeystack` exports were added in 0.2.0.
|
|
92
|
+
- The pre-0.1.0 top-level default `initHeystack({ apiKey })` is **gone**. Use the subpath entries: `@heystack/otel/node`, `@heystack/otel/next`, `@heystack/otel/workers`. The root `@heystack/otel` entry now exposes only pure helpers (`buildExporterConfig`, types).
|
|
93
|
+
|
|
94
|
+
## Verify it's working
|
|
95
|
+
|
|
96
|
+
- Pass `debug: true` to `initHeystack` to print OTel diagnostic/export logs to the console.
|
|
97
|
+
- Short-lived processes: `initHeystack` registers `SIGTERM`/`SIGINT` handlers automatically, so the SDK flushes and shuts down before exit and you don't lose the last batch.
|
|
98
|
+
|
|
15
99
|
Then run your app and make a request — traces appear in your Heystack dashboard within seconds.
|
package/dist/core.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export declare const DEFAULT_ENDPOINT = "https://ingest.heystack.dev";
|
|
2
|
+
export interface HeystackOptions {
|
|
3
|
+
/** Your Heystack ingest key (sk_live_...). Prefer process.env.HEYSTACK_API_KEY. */
|
|
4
|
+
apiKey: string;
|
|
5
|
+
/** OTel service.name this app reports as. */
|
|
6
|
+
service: string;
|
|
7
|
+
/** Override the ingest endpoint (defaults to Heystack production). */
|
|
8
|
+
endpoint?: string;
|
|
9
|
+
}
|
|
10
|
+
export interface ExporterConfig {
|
|
11
|
+
url: string;
|
|
12
|
+
headers: {
|
|
13
|
+
Authorization: string;
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
/** Pure: derive the OTLP/HTTP traces URL + auth header. Works in any runtime. */
|
|
17
|
+
export declare function buildExporterConfig(o: HeystackOptions): ExporterConfig;
|
package/dist/core.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export const DEFAULT_ENDPOINT = "https://ingest.heystack.dev";
|
|
2
|
+
/** Pure: derive the OTLP/HTTP traces URL + auth header. Works in any runtime. */
|
|
3
|
+
export function buildExporterConfig(o) {
|
|
4
|
+
const base = (o.endpoint ?? DEFAULT_ENDPOINT).replace(/\/+$/, "");
|
|
5
|
+
return { url: `${base}/v1/traces`, headers: { Authorization: `Bearer ${o.apiKey}` } };
|
|
6
|
+
}
|
package/dist/next.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { HeystackOptions } from "./core.js";
|
|
2
|
+
/**
|
|
3
|
+
* Call from Next.js instrumentation.ts:
|
|
4
|
+
* export function register() { registerHeystack({ service: "my-app" }); }
|
|
5
|
+
* apiKey defaults to process.env.HEYSTACK_API_KEY.
|
|
6
|
+
*
|
|
7
|
+
* Runtime-aware: this runs on the Node.js runtime (NEXT_RUNTIME === "nodejs"),
|
|
8
|
+
* but that runtime can actually be Cloudflare workerd under OpenNext — where the
|
|
9
|
+
* Node SDK's node:http OTLP exporter silently sends nothing. We detect workerd
|
|
10
|
+
* and use the fetch-based exporter (@heystack/otel/workers) instead. On the Edge
|
|
11
|
+
* runtime neither SDK can run, so it's a no-op.
|
|
12
|
+
*
|
|
13
|
+
* FLUSH (workerd): there is no per-request `ExecutionContext` on this path, so
|
|
14
|
+
* spans are NOT auto-flushed and the export fetch may be cut off when the
|
|
15
|
+
* isolate is torn down. For guaranteed delivery, import and call
|
|
16
|
+
* `flushHeystack()` from `@heystack/otel/workers` in a response hook (or via
|
|
17
|
+
* `ctx.waitUntil(flushHeystack())` if you have a ctx) — it awaits the export.
|
|
18
|
+
*/
|
|
19
|
+
export declare function registerHeystack(o: Partial<HeystackOptions> & {
|
|
20
|
+
service: string;
|
|
21
|
+
}): Promise<void>;
|
package/dist/next.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Call from Next.js instrumentation.ts:
|
|
3
|
+
* export function register() { registerHeystack({ service: "my-app" }); }
|
|
4
|
+
* apiKey defaults to process.env.HEYSTACK_API_KEY.
|
|
5
|
+
*
|
|
6
|
+
* Runtime-aware: this runs on the Node.js runtime (NEXT_RUNTIME === "nodejs"),
|
|
7
|
+
* but that runtime can actually be Cloudflare workerd under OpenNext — where the
|
|
8
|
+
* Node SDK's node:http OTLP exporter silently sends nothing. We detect workerd
|
|
9
|
+
* and use the fetch-based exporter (@heystack/otel/workers) instead. On the Edge
|
|
10
|
+
* runtime neither SDK can run, so it's a no-op.
|
|
11
|
+
*
|
|
12
|
+
* FLUSH (workerd): there is no per-request `ExecutionContext` on this path, so
|
|
13
|
+
* spans are NOT auto-flushed and the export fetch may be cut off when the
|
|
14
|
+
* isolate is torn down. For guaranteed delivery, import and call
|
|
15
|
+
* `flushHeystack()` from `@heystack/otel/workers` in a response hook (or via
|
|
16
|
+
* `ctx.waitUntil(flushHeystack())` if you have a ctx) — it awaits the export.
|
|
17
|
+
*/
|
|
18
|
+
export async function registerHeystack(o) {
|
|
19
|
+
// Edge runtime can't run either SDK path — skip.
|
|
20
|
+
if (process.env.NEXT_RUNTIME === "edge")
|
|
21
|
+
return;
|
|
22
|
+
const apiKey = o.apiKey ?? process.env.HEYSTACK_API_KEY;
|
|
23
|
+
if (!apiKey) {
|
|
24
|
+
console.warn("[heystack] HEYSTACK_API_KEY not set — tracing disabled");
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
// Detect Cloudflare workerd (OpenNext / Workers). NEXT_RUNTIME is "nodejs" there,
|
|
28
|
+
// but the Node SDK's node:http exporter can't send — use the fetch exporter instead.
|
|
29
|
+
const g = globalThis;
|
|
30
|
+
const onWorkerd = g.navigator?.userAgent === "Cloudflare-Workers" ||
|
|
31
|
+
(typeof g.caches?.default !== "undefined" && !g.process?.versions?.node);
|
|
32
|
+
if (onWorkerd) {
|
|
33
|
+
const { initHeystackWorkers } = await import("./workers.js");
|
|
34
|
+
initHeystackWorkers({ apiKey, service: o.service, endpoint: o.endpoint });
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
const { initHeystack } = await import("./node.js");
|
|
38
|
+
initHeystack({
|
|
39
|
+
apiKey,
|
|
40
|
+
service: o.service,
|
|
41
|
+
endpoint: o.endpoint,
|
|
42
|
+
debug: o.debug,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
}
|
package/dist/node.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { NodeSDK } from "@opentelemetry/sdk-node";
|
|
2
|
+
import { type HeystackOptions } from "./core.js";
|
|
3
|
+
export interface NodeOptions extends HeystackOptions {
|
|
4
|
+
/** Enable OTel diagnostic logging to console to confirm export. */
|
|
5
|
+
debug?: boolean;
|
|
6
|
+
/** Set false to skip auto-instrumentations (you'll then only get framework/manual spans). Default true. */
|
|
7
|
+
autoInstrument?: boolean;
|
|
8
|
+
}
|
|
9
|
+
/** Initialise Heystack tracing on a Node runtime. Call once, as early as possible. Returns the started SDK. */
|
|
10
|
+
export declare function initHeystack(o: NodeOptions): NodeSDK;
|
|
11
|
+
/** Flush + shutdown the SDK on SIGTERM/SIGINT so short-lived processes don't lose the last batch. */
|
|
12
|
+
export declare function shutdownOnSignals(sdk: NodeSDK): void;
|
package/dist/node.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { NodeSDK } from "@opentelemetry/sdk-node";
|
|
2
|
+
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
|
|
3
|
+
import { getNodeAutoInstrumentations } from "@opentelemetry/auto-instrumentations-node";
|
|
4
|
+
import { diag, DiagConsoleLogger, DiagLogLevel } from "@opentelemetry/api";
|
|
5
|
+
import { buildExporterConfig } from "./core.js";
|
|
6
|
+
/** Initialise Heystack tracing on a Node runtime. Call once, as early as possible. Returns the started SDK. */
|
|
7
|
+
export function initHeystack(o) {
|
|
8
|
+
if (o.debug)
|
|
9
|
+
diag.setLogger(new DiagConsoleLogger(), DiagLogLevel.DEBUG);
|
|
10
|
+
const cfg = buildExporterConfig(o);
|
|
11
|
+
const sdk = new NodeSDK({
|
|
12
|
+
serviceName: o.service,
|
|
13
|
+
traceExporter: new OTLPTraceExporter({ url: cfg.url, headers: cfg.headers }),
|
|
14
|
+
instrumentations: o.autoInstrument === false ? [] : [getNodeAutoInstrumentations()],
|
|
15
|
+
});
|
|
16
|
+
sdk.start();
|
|
17
|
+
shutdownOnSignals(sdk);
|
|
18
|
+
return sdk;
|
|
19
|
+
}
|
|
20
|
+
/** Flush + shutdown the SDK on SIGTERM/SIGINT so short-lived processes don't lose the last batch. */
|
|
21
|
+
export function shutdownOnSignals(sdk) {
|
|
22
|
+
const stop = () => {
|
|
23
|
+
sdk
|
|
24
|
+
.shutdown()
|
|
25
|
+
.catch(() => { })
|
|
26
|
+
.finally(() => process.exit(0));
|
|
27
|
+
};
|
|
28
|
+
process.once("SIGTERM", stop);
|
|
29
|
+
process.once("SIGINT", stop);
|
|
30
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { type Span } from "@opentelemetry/api";
|
|
2
|
+
import { BasicTracerProvider, type ReadableSpan, type SpanExporter } from "@opentelemetry/sdk-trace-base";
|
|
3
|
+
import { type HeystackOptions } from "./core.js";
|
|
4
|
+
declare const ExportResultCode: {
|
|
5
|
+
readonly SUCCESS: 0;
|
|
6
|
+
readonly FAILED: 1;
|
|
7
|
+
};
|
|
8
|
+
interface ExportResult {
|
|
9
|
+
code: (typeof ExportResultCode)[keyof typeof ExportResultCode];
|
|
10
|
+
error?: Error;
|
|
11
|
+
}
|
|
12
|
+
interface OtlpAnyValue {
|
|
13
|
+
stringValue?: string;
|
|
14
|
+
intValue?: string;
|
|
15
|
+
doubleValue?: number;
|
|
16
|
+
boolValue?: boolean;
|
|
17
|
+
arrayValue?: {
|
|
18
|
+
values: OtlpAnyValue[];
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
interface OtlpKeyValue {
|
|
22
|
+
key: string;
|
|
23
|
+
value: OtlpAnyValue;
|
|
24
|
+
}
|
|
25
|
+
interface OtlpSpan {
|
|
26
|
+
traceId: string;
|
|
27
|
+
spanId: string;
|
|
28
|
+
parentSpanId: string;
|
|
29
|
+
name: string;
|
|
30
|
+
kind: number;
|
|
31
|
+
startTimeUnixNano: string;
|
|
32
|
+
endTimeUnixNano: string;
|
|
33
|
+
attributes: OtlpKeyValue[];
|
|
34
|
+
status: {
|
|
35
|
+
code: number;
|
|
36
|
+
message?: string;
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
interface OtlpTracesPayload {
|
|
40
|
+
resourceSpans: {
|
|
41
|
+
resource: {
|
|
42
|
+
attributes: OtlpKeyValue[];
|
|
43
|
+
};
|
|
44
|
+
scopeSpans: {
|
|
45
|
+
spans: OtlpSpan[];
|
|
46
|
+
}[];
|
|
47
|
+
}[];
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Serialize a batch of ended spans into a single OTLP/JSON traces payload.
|
|
51
|
+
*
|
|
52
|
+
* All spans in a SimpleSpanProcessor batch share the same per-request provider,
|
|
53
|
+
* hence the same resource, so we emit a single resourceSpans entry.
|
|
54
|
+
*/
|
|
55
|
+
export declare function serializeSpans(spans: ReadableSpan[]): OtlpTracesPayload;
|
|
56
|
+
/**
|
|
57
|
+
* A WinterCG-compatible OTLP/JSON span exporter. POSTs ended spans to the
|
|
58
|
+
* Heystack ingest using the platform `fetch` — no Node built-ins.
|
|
59
|
+
*/
|
|
60
|
+
export declare class HeystackSpanExporter implements SpanExporter {
|
|
61
|
+
private readonly url;
|
|
62
|
+
private readonly headers;
|
|
63
|
+
private shutdownState;
|
|
64
|
+
/**
|
|
65
|
+
* In-flight export fetches. Each `export()` adds its settled-when-the-POST-
|
|
66
|
+
* completes promise here and removes it on `finally`. `forceFlush()` awaits
|
|
67
|
+
* this set so callers (and `ctx.waitUntil`) genuinely wait for the network
|
|
68
|
+
* write before the isolate is torn down — otherwise traces are silently
|
|
69
|
+
* dropped on fast-responding Workers/edge handlers.
|
|
70
|
+
*/
|
|
71
|
+
private readonly pending;
|
|
72
|
+
constructor(options: HeystackOptions);
|
|
73
|
+
export(spans: ReadableSpan[], resultCallback: (result: ExportResult) => void): void;
|
|
74
|
+
shutdown(): Promise<void>;
|
|
75
|
+
/**
|
|
76
|
+
* Resolve only once every in-flight export fetch has settled. This is the
|
|
77
|
+
* guaranteed drain path: the OTel `SimpleSpanProcessor.forceFlush()` does not
|
|
78
|
+
* await our fire-and-forget fetch, so callers must await this directly (see
|
|
79
|
+
* `flushHeystack` and `instrument`).
|
|
80
|
+
*/
|
|
81
|
+
forceFlush(): Promise<void>;
|
|
82
|
+
}
|
|
83
|
+
export interface WorkersConfig {
|
|
84
|
+
service: string;
|
|
85
|
+
/** Defaults to env.HEYSTACK_API_KEY at request time if omitted. */
|
|
86
|
+
apiKey?: string;
|
|
87
|
+
endpoint?: string;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* A `BasicTracerProvider` with the underlying `HeystackSpanExporter` attached so
|
|
91
|
+
* flush paths can drain its in-flight fetches directly (the OTel processor's
|
|
92
|
+
* `forceFlush()` does not await our fire-and-forget fetch).
|
|
93
|
+
*/
|
|
94
|
+
export type HeystackTracerProvider = BasicTracerProvider & {
|
|
95
|
+
readonly heystackExporter: HeystackSpanExporter;
|
|
96
|
+
};
|
|
97
|
+
/**
|
|
98
|
+
* Build a `BasicTracerProvider` wired to a `HeystackSpanExporter` via a
|
|
99
|
+
* `SimpleSpanProcessor`. Exposed for advanced users who want to add manual
|
|
100
|
+
* spans; for typical use prefer {@link instrument}.
|
|
101
|
+
*
|
|
102
|
+
* On Workers each request should get its own short-lived provider so its spans
|
|
103
|
+
* can be flushed. IMPORTANT: to actually wait for the export network write you
|
|
104
|
+
* must drain the exporter — `await provider.heystackExporter.forceFlush()` (or
|
|
105
|
+
* `flushHeystack()`), not just `provider.forceFlush()`.
|
|
106
|
+
*/
|
|
107
|
+
export declare function createTracerProvider(config: HeystackOptions): HeystackTracerProvider;
|
|
108
|
+
/**
|
|
109
|
+
* Register Heystack as the global tracer provider on a Workers/edge runtime
|
|
110
|
+
* (workerd). Spans from the host framework (e.g. Next.js) export over fetch.
|
|
111
|
+
* Use this instead of @heystack/otel/node when running on Cloudflare/edge.
|
|
112
|
+
*
|
|
113
|
+
* FLUSH: there is no per-request `ExecutionContext` on this path, so spans are
|
|
114
|
+
* NOT auto-flushed. To guarantee delivery on workerd, call `flushHeystack()`
|
|
115
|
+
* (which awaits the export fetch) from a response hook — or hand it to
|
|
116
|
+
* `ctx.waitUntil(flushHeystack())` if you do have a ctx. Returns the provider.
|
|
117
|
+
*/
|
|
118
|
+
export declare function initHeystackWorkers(config: WorkersConfig & {
|
|
119
|
+
apiKey: string;
|
|
120
|
+
}): HeystackTracerProvider;
|
|
121
|
+
/**
|
|
122
|
+
* Force-flush any pending spans AND wait for the export network write to
|
|
123
|
+
* complete. This awaits both the OTel provider's `forceFlush()` (drains the
|
|
124
|
+
* span processor) and the exporter's own pending-fetch set (the part the OTel
|
|
125
|
+
* processor does not await) — so on workerd you can hand this to
|
|
126
|
+
* `ctx.waitUntil(flushHeystack())` and the POST genuinely finishes before the
|
|
127
|
+
* isolate is torn down. Best-effort no-op if no provider is registered.
|
|
128
|
+
*/
|
|
129
|
+
export declare function flushHeystack(): Promise<void>;
|
|
130
|
+
/** Reset the singleton global provider. Internal/testing helper. */
|
|
131
|
+
export declare function __resetProvider(): void;
|
|
132
|
+
/** Reset the once-only no-key warning. Internal/testing helper. */
|
|
133
|
+
export declare function __resetWarnings(): void;
|
|
134
|
+
interface FetchHandler<E> {
|
|
135
|
+
fetch: (req: Request, env: E, ctx: ExecutionContext) => Promise<Response> | Response;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Wrap a Worker's default export so every request is auto-traced with a SERVER
|
|
139
|
+
* span.
|
|
140
|
+
*
|
|
141
|
+
* FLUSH (CRITICAL on Workers/edge): the export is a `fetch()` POST. After
|
|
142
|
+
* `span.end()` we `ctx.waitUntil` a promise that awaits BOTH the provider's
|
|
143
|
+
* span processor AND the exporter's in-flight fetch, so the network write
|
|
144
|
+
* completes before the isolate is torn down. Without this, fast-responding
|
|
145
|
+
* handlers return before the POST finishes and the trace is silently dropped.
|
|
146
|
+
*
|
|
147
|
+
* import { instrument } from "@heystack/otel/workers";
|
|
148
|
+
* export default instrument(
|
|
149
|
+
* { async fetch(req, env, ctx) { return new Response("ok"); } },
|
|
150
|
+
* { service: "my-worker" },
|
|
151
|
+
* );
|
|
152
|
+
*
|
|
153
|
+
* If no API key is available (neither `config.apiKey` nor
|
|
154
|
+
* `env.HEYSTACK_API_KEY`), the handler runs untraced.
|
|
155
|
+
*/
|
|
156
|
+
export declare function instrument<E = unknown>(handler: FetchHandler<E>, config: WorkersConfig): {
|
|
157
|
+
fetch: (req: Request, env: E, ctx: ExecutionContext) => Promise<Response>;
|
|
158
|
+
};
|
|
159
|
+
export type { Span };
|
package/dist/workers.js
ADDED
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
/// <reference types="@cloudflare/workers-types" />
|
|
2
|
+
//
|
|
3
|
+
// Native Cloudflare Workers / Vercel Edge tracing path for Heystack.
|
|
4
|
+
//
|
|
5
|
+
// Unlike `@heystack/otel/node`, this entry contains NO Node built-ins. It uses
|
|
6
|
+
// only `@opentelemetry/api`, `@opentelemetry/sdk-trace-base`,
|
|
7
|
+
// `@opentelemetry/resources` and the platform `fetch` — all WinterCG-safe — and
|
|
8
|
+
// ships its own OTLP/JSON-over-fetch span exporter so it runs on Workers/Edge
|
|
9
|
+
// where the Node SDK cannot.
|
|
10
|
+
import { context, trace, SpanKind, SpanStatusCode, } from "@opentelemetry/api";
|
|
11
|
+
import { Resource } from "@opentelemetry/resources";
|
|
12
|
+
import { BasicTracerProvider, SimpleSpanProcessor, } from "@opentelemetry/sdk-trace-base";
|
|
13
|
+
import { ATTR_SERVICE_NAME } from "@opentelemetry/semantic-conventions";
|
|
14
|
+
import { buildExporterConfig } from "./core.js";
|
|
15
|
+
// `ExportResult` / `ExportResultCode` mirror `@opentelemetry/core`. We define
|
|
16
|
+
// them inline (structurally identical) rather than import them: core is only a
|
|
17
|
+
// transitive dep of sdk-trace-base and isn't reliably resolvable, and keeping it
|
|
18
|
+
// out guarantees no extra (potentially node-platform) code in the bundle.
|
|
19
|
+
const ExportResultCode = { SUCCESS: 0, FAILED: 1 };
|
|
20
|
+
/** Convert an OTel HrTime `[seconds, nanos]` tuple to a nanosecond string. */
|
|
21
|
+
function hrTimeToUnixNano(time) {
|
|
22
|
+
// BigInt math keeps full nanosecond precision without float rounding.
|
|
23
|
+
return (BigInt(time[0]) * 1000000000n + BigInt(time[1])).toString();
|
|
24
|
+
}
|
|
25
|
+
/** Map a single attribute value to the OTLP/JSON tagged-union form. */
|
|
26
|
+
function toAnyValue(value) {
|
|
27
|
+
if (typeof value === "string")
|
|
28
|
+
return { stringValue: value };
|
|
29
|
+
if (typeof value === "boolean")
|
|
30
|
+
return { boolValue: value };
|
|
31
|
+
if (typeof value === "number") {
|
|
32
|
+
// OTLP distinguishes int from double; Number.isInteger picks the int tag.
|
|
33
|
+
return Number.isInteger(value)
|
|
34
|
+
? { intValue: String(value) }
|
|
35
|
+
: { doubleValue: value };
|
|
36
|
+
}
|
|
37
|
+
if (Array.isArray(value)) {
|
|
38
|
+
return { arrayValue: { values: value.map(toAnyValue) } };
|
|
39
|
+
}
|
|
40
|
+
// Fallback: stringify anything else (objects, bigint, etc.).
|
|
41
|
+
return { stringValue: String(value) };
|
|
42
|
+
}
|
|
43
|
+
function toKeyValues(attrs) {
|
|
44
|
+
const out = [];
|
|
45
|
+
if (!attrs)
|
|
46
|
+
return out;
|
|
47
|
+
for (const key of Object.keys(attrs)) {
|
|
48
|
+
const v = attrs[key];
|
|
49
|
+
if (v === undefined || v === null)
|
|
50
|
+
continue;
|
|
51
|
+
out.push({ key, value: toAnyValue(v) });
|
|
52
|
+
}
|
|
53
|
+
return out;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Map the OTel API `SpanKind` enum to the OTLP wire encoding.
|
|
57
|
+
*
|
|
58
|
+
* The API enum is INTERNAL=0, SERVER=1, CLIENT=2, PRODUCER=3, CONSUMER=4, but
|
|
59
|
+
* the OTLP/JSON wire format (and Heystack's schema `SPAN_KIND` map) is
|
|
60
|
+
* UNSPECIFIED=0, INTERNAL=1, SERVER=2, CLIENT=3, PRODUCER=4, CONSUMER=5 — i.e.
|
|
61
|
+
* shifted by one. Without this shift, SERVER spans would be ingested as
|
|
62
|
+
* INTERNAL.
|
|
63
|
+
*/
|
|
64
|
+
function toOtlpKind(kind) {
|
|
65
|
+
return kind + 1;
|
|
66
|
+
}
|
|
67
|
+
function toStatus(status) {
|
|
68
|
+
// SpanStatusCode is the int enum (UNSET=0, OK=1, ERROR=2) the schema expects.
|
|
69
|
+
const out = { code: status.code };
|
|
70
|
+
if (status.message)
|
|
71
|
+
out.message = status.message;
|
|
72
|
+
return out;
|
|
73
|
+
}
|
|
74
|
+
function readableSpanToOtlp(span) {
|
|
75
|
+
const ctx = span.spanContext();
|
|
76
|
+
return {
|
|
77
|
+
traceId: ctx.traceId,
|
|
78
|
+
spanId: ctx.spanId,
|
|
79
|
+
parentSpanId: span.parentSpanId ?? "",
|
|
80
|
+
name: span.name,
|
|
81
|
+
kind: toOtlpKind(span.kind),
|
|
82
|
+
startTimeUnixNano: hrTimeToUnixNano(span.startTime),
|
|
83
|
+
endTimeUnixNano: hrTimeToUnixNano(span.endTime),
|
|
84
|
+
attributes: toKeyValues(span.attributes),
|
|
85
|
+
status: toStatus(span.status),
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Serialize a batch of ended spans into a single OTLP/JSON traces payload.
|
|
90
|
+
*
|
|
91
|
+
* All spans in a SimpleSpanProcessor batch share the same per-request provider,
|
|
92
|
+
* hence the same resource, so we emit a single resourceSpans entry.
|
|
93
|
+
*/
|
|
94
|
+
export function serializeSpans(spans) {
|
|
95
|
+
const resourceAttrs = spans[0]
|
|
96
|
+
? spans[0].resource.attributes
|
|
97
|
+
: {};
|
|
98
|
+
return {
|
|
99
|
+
resourceSpans: [
|
|
100
|
+
{
|
|
101
|
+
resource: { attributes: toKeyValues(resourceAttrs) },
|
|
102
|
+
scopeSpans: [{ spans: spans.map(readableSpanToOtlp) }],
|
|
103
|
+
},
|
|
104
|
+
],
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
// ---------------------------------------------------------------------------
|
|
108
|
+
// Exporter
|
|
109
|
+
// ---------------------------------------------------------------------------
|
|
110
|
+
/**
|
|
111
|
+
* A WinterCG-compatible OTLP/JSON span exporter. POSTs ended spans to the
|
|
112
|
+
* Heystack ingest using the platform `fetch` — no Node built-ins.
|
|
113
|
+
*/
|
|
114
|
+
export class HeystackSpanExporter {
|
|
115
|
+
url;
|
|
116
|
+
headers;
|
|
117
|
+
shutdownState = false;
|
|
118
|
+
/**
|
|
119
|
+
* In-flight export fetches. Each `export()` adds its settled-when-the-POST-
|
|
120
|
+
* completes promise here and removes it on `finally`. `forceFlush()` awaits
|
|
121
|
+
* this set so callers (and `ctx.waitUntil`) genuinely wait for the network
|
|
122
|
+
* write before the isolate is torn down — otherwise traces are silently
|
|
123
|
+
* dropped on fast-responding Workers/edge handlers.
|
|
124
|
+
*/
|
|
125
|
+
pending = new Set();
|
|
126
|
+
constructor(options) {
|
|
127
|
+
const cfg = buildExporterConfig(options);
|
|
128
|
+
this.url = cfg.url;
|
|
129
|
+
this.headers = {
|
|
130
|
+
...cfg.headers,
|
|
131
|
+
"content-type": "application/json",
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
export(spans, resultCallback) {
|
|
135
|
+
if (this.shutdownState) {
|
|
136
|
+
resultCallback({
|
|
137
|
+
code: ExportResultCode.FAILED,
|
|
138
|
+
error: new Error("HeystackSpanExporter has been shut down"),
|
|
139
|
+
});
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
if (spans.length === 0) {
|
|
143
|
+
resultCallback({ code: ExportResultCode.SUCCESS });
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
const body = JSON.stringify(serializeSpans(spans));
|
|
147
|
+
// Build the fetch chain as a promise we retain, so forceFlush() can await
|
|
148
|
+
// the actual network write. It resolves (never rejects) once the POST has
|
|
149
|
+
// completed (success or fail) and resultCallback has been invoked.
|
|
150
|
+
const p = fetch(this.url, { method: "POST", headers: this.headers, body })
|
|
151
|
+
.then((res) => {
|
|
152
|
+
if (res.ok) {
|
|
153
|
+
resultCallback({ code: ExportResultCode.SUCCESS });
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
resultCallback({
|
|
157
|
+
code: ExportResultCode.FAILED,
|
|
158
|
+
error: new Error(`Heystack ingest responded ${res.status} ${res.statusText}`),
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
})
|
|
162
|
+
.catch((error) => {
|
|
163
|
+
resultCallback({
|
|
164
|
+
code: ExportResultCode.FAILED,
|
|
165
|
+
error: error instanceof Error ? error : new Error(String(error)),
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
this.pending.add(p);
|
|
169
|
+
p.finally(() => this.pending.delete(p));
|
|
170
|
+
}
|
|
171
|
+
shutdown() {
|
|
172
|
+
return this.forceFlush().then(() => {
|
|
173
|
+
this.shutdownState = true;
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Resolve only once every in-flight export fetch has settled. This is the
|
|
178
|
+
* guaranteed drain path: the OTel `SimpleSpanProcessor.forceFlush()` does not
|
|
179
|
+
* await our fire-and-forget fetch, so callers must await this directly (see
|
|
180
|
+
* `flushHeystack` and `instrument`).
|
|
181
|
+
*/
|
|
182
|
+
async forceFlush() {
|
|
183
|
+
await Promise.allSettled([...this.pending]);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Build a `BasicTracerProvider` wired to a `HeystackSpanExporter` via a
|
|
188
|
+
* `SimpleSpanProcessor`. Exposed for advanced users who want to add manual
|
|
189
|
+
* spans; for typical use prefer {@link instrument}.
|
|
190
|
+
*
|
|
191
|
+
* On Workers each request should get its own short-lived provider so its spans
|
|
192
|
+
* can be flushed. IMPORTANT: to actually wait for the export network write you
|
|
193
|
+
* must drain the exporter — `await provider.heystackExporter.forceFlush()` (or
|
|
194
|
+
* `flushHeystack()`), not just `provider.forceFlush()`.
|
|
195
|
+
*/
|
|
196
|
+
export function createTracerProvider(config) {
|
|
197
|
+
const exporter = new HeystackSpanExporter(config);
|
|
198
|
+
const provider = new BasicTracerProvider({
|
|
199
|
+
resource: new Resource({ [ATTR_SERVICE_NAME]: config.service }),
|
|
200
|
+
spanProcessors: [new SimpleSpanProcessor(exporter)],
|
|
201
|
+
});
|
|
202
|
+
// Attach the exporter so flush paths can await its in-flight fetches.
|
|
203
|
+
return Object.assign(provider, { heystackExporter: exporter });
|
|
204
|
+
}
|
|
205
|
+
// ---------------------------------------------------------------------------
|
|
206
|
+
// Global tracer provider registration (for host frameworks, e.g. Next.js)
|
|
207
|
+
// ---------------------------------------------------------------------------
|
|
208
|
+
let _provider = null;
|
|
209
|
+
/**
|
|
210
|
+
* Register Heystack as the global tracer provider on a Workers/edge runtime
|
|
211
|
+
* (workerd). Spans from the host framework (e.g. Next.js) export over fetch.
|
|
212
|
+
* Use this instead of @heystack/otel/node when running on Cloudflare/edge.
|
|
213
|
+
*
|
|
214
|
+
* FLUSH: there is no per-request `ExecutionContext` on this path, so spans are
|
|
215
|
+
* NOT auto-flushed. To guarantee delivery on workerd, call `flushHeystack()`
|
|
216
|
+
* (which awaits the export fetch) from a response hook — or hand it to
|
|
217
|
+
* `ctx.waitUntil(flushHeystack())` if you do have a ctx. Returns the provider.
|
|
218
|
+
*/
|
|
219
|
+
export function initHeystackWorkers(config) {
|
|
220
|
+
if (_provider)
|
|
221
|
+
return _provider;
|
|
222
|
+
_provider = createTracerProvider(config);
|
|
223
|
+
// register as the global provider so framework spans flow to our exporter.
|
|
224
|
+
// BasicTracerProvider.register() also wires context/propagation, but on
|
|
225
|
+
// workerd we only need the global tracer provider — set it directly so it's
|
|
226
|
+
// deterministic and doesn't pull in a context manager that may not run here.
|
|
227
|
+
trace.setGlobalTracerProvider(_provider);
|
|
228
|
+
return _provider;
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Force-flush any pending spans AND wait for the export network write to
|
|
232
|
+
* complete. This awaits both the OTel provider's `forceFlush()` (drains the
|
|
233
|
+
* span processor) and the exporter's own pending-fetch set (the part the OTel
|
|
234
|
+
* processor does not await) — so on workerd you can hand this to
|
|
235
|
+
* `ctx.waitUntil(flushHeystack())` and the POST genuinely finishes before the
|
|
236
|
+
* isolate is torn down. Best-effort no-op if no provider is registered.
|
|
237
|
+
*/
|
|
238
|
+
export async function flushHeystack() {
|
|
239
|
+
if (!_provider)
|
|
240
|
+
return;
|
|
241
|
+
if (typeof _provider.forceFlush === "function") {
|
|
242
|
+
await _provider.forceFlush();
|
|
243
|
+
}
|
|
244
|
+
// The processor's forceFlush does not await our fire-and-forget fetch, so
|
|
245
|
+
// drain the exporter's in-flight POSTs explicitly.
|
|
246
|
+
await _provider.heystackExporter.forceFlush();
|
|
247
|
+
}
|
|
248
|
+
/** Reset the singleton global provider. Internal/testing helper. */
|
|
249
|
+
export function __resetProvider() {
|
|
250
|
+
_provider = null;
|
|
251
|
+
}
|
|
252
|
+
let warnedNoKey = false;
|
|
253
|
+
function warnOnceNoKey() {
|
|
254
|
+
if (warnedNoKey)
|
|
255
|
+
return;
|
|
256
|
+
warnedNoKey = true;
|
|
257
|
+
console.warn("[heystack] HEYSTACK_API_KEY not set — Workers tracing disabled, handler runs untraced.");
|
|
258
|
+
}
|
|
259
|
+
/** Reset the once-only no-key warning. Internal/testing helper. */
|
|
260
|
+
export function __resetWarnings() {
|
|
261
|
+
warnedNoKey = false;
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Wrap a Worker's default export so every request is auto-traced with a SERVER
|
|
265
|
+
* span.
|
|
266
|
+
*
|
|
267
|
+
* FLUSH (CRITICAL on Workers/edge): the export is a `fetch()` POST. After
|
|
268
|
+
* `span.end()` we `ctx.waitUntil` a promise that awaits BOTH the provider's
|
|
269
|
+
* span processor AND the exporter's in-flight fetch, so the network write
|
|
270
|
+
* completes before the isolate is torn down. Without this, fast-responding
|
|
271
|
+
* handlers return before the POST finishes and the trace is silently dropped.
|
|
272
|
+
*
|
|
273
|
+
* import { instrument } from "@heystack/otel/workers";
|
|
274
|
+
* export default instrument(
|
|
275
|
+
* { async fetch(req, env, ctx) { return new Response("ok"); } },
|
|
276
|
+
* { service: "my-worker" },
|
|
277
|
+
* );
|
|
278
|
+
*
|
|
279
|
+
* If no API key is available (neither `config.apiKey` nor
|
|
280
|
+
* `env.HEYSTACK_API_KEY`), the handler runs untraced.
|
|
281
|
+
*/
|
|
282
|
+
export function instrument(handler, config) {
|
|
283
|
+
return {
|
|
284
|
+
async fetch(req, env, ctx) {
|
|
285
|
+
const apiKey = config.apiKey ?? env?.HEYSTACK_API_KEY;
|
|
286
|
+
// No key → run untraced (warn once).
|
|
287
|
+
if (!apiKey) {
|
|
288
|
+
warnOnceNoKey();
|
|
289
|
+
return handler.fetch(req, env, ctx);
|
|
290
|
+
}
|
|
291
|
+
const provider = createTracerProvider({
|
|
292
|
+
apiKey,
|
|
293
|
+
service: config.service,
|
|
294
|
+
endpoint: config.endpoint,
|
|
295
|
+
});
|
|
296
|
+
const tracer = provider.getTracer("@heystack/otel/workers");
|
|
297
|
+
const url = new URL(req.url);
|
|
298
|
+
const span = tracer.startSpan(`${req.method} ${url.pathname}`, {
|
|
299
|
+
kind: SpanKind.SERVER,
|
|
300
|
+
attributes: {
|
|
301
|
+
"http.request.method": req.method,
|
|
302
|
+
"url.full": req.url,
|
|
303
|
+
"url.path": url.pathname,
|
|
304
|
+
"server.address": url.host,
|
|
305
|
+
},
|
|
306
|
+
});
|
|
307
|
+
// waitUntil a promise that drains BOTH the provider's span processor and
|
|
308
|
+
// the exporter's in-flight fetch. Awaiting only provider.forceFlush()
|
|
309
|
+
// would return before the export POST completes, letting the isolate be
|
|
310
|
+
// torn down and silently dropping the trace.
|
|
311
|
+
const flush = () => ctx.waitUntil((async () => {
|
|
312
|
+
await provider.forceFlush().catch(() => { });
|
|
313
|
+
await provider.heystackExporter.forceFlush().catch(() => { });
|
|
314
|
+
})());
|
|
315
|
+
try {
|
|
316
|
+
const response = await context.with(trace.setSpan(context.active(), span), () => handler.fetch(req, env, ctx));
|
|
317
|
+
span.setAttribute("http.response.status_code", response.status);
|
|
318
|
+
span.setStatus({
|
|
319
|
+
code: response.status >= 500 ? SpanStatusCode.ERROR : SpanStatusCode.UNSET,
|
|
320
|
+
});
|
|
321
|
+
span.end();
|
|
322
|
+
flush();
|
|
323
|
+
return response;
|
|
324
|
+
}
|
|
325
|
+
catch (error) {
|
|
326
|
+
if (error instanceof Error)
|
|
327
|
+
span.recordException(error);
|
|
328
|
+
span.setStatus({
|
|
329
|
+
code: SpanStatusCode.ERROR,
|
|
330
|
+
message: error instanceof Error ? error.message : String(error),
|
|
331
|
+
});
|
|
332
|
+
span.end();
|
|
333
|
+
flush();
|
|
334
|
+
throw error;
|
|
335
|
+
}
|
|
336
|
+
},
|
|
337
|
+
};
|
|
338
|
+
}
|
package/package.json
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@heystack/otel",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.2.1",
|
|
4
|
+
"description": "Runtime-aware OpenTelemetry tracing that exports to Heystack (Node, Next.js, Workers).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
|
-
"main": "./dist/
|
|
8
|
-
"types": "./dist/
|
|
9
|
-
"exports": {
|
|
7
|
+
"main": "./dist/core.js",
|
|
8
|
+
"types": "./dist/core.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": { "types": "./dist/core.d.ts", "import": "./dist/core.js" },
|
|
11
|
+
"./node": { "types": "./dist/node.d.ts", "import": "./dist/node.js" },
|
|
12
|
+
"./next": { "types": "./dist/next.d.ts", "import": "./dist/next.js" },
|
|
13
|
+
"./workers": { "types": "./dist/workers.d.ts", "import": "./dist/workers.js" }
|
|
14
|
+
},
|
|
10
15
|
"files": ["dist", "README.md"],
|
|
11
16
|
"publishConfig": { "access": "public" },
|
|
12
17
|
"scripts": {
|
|
@@ -16,14 +21,18 @@
|
|
|
16
21
|
"prepublishOnly": "pnpm build"
|
|
17
22
|
},
|
|
18
23
|
"dependencies": {
|
|
24
|
+
"@opentelemetry/api": "^1.9.0",
|
|
19
25
|
"@opentelemetry/sdk-node": "^0.57.0",
|
|
20
26
|
"@opentelemetry/exporter-trace-otlp-http": "^0.57.0",
|
|
27
|
+
"@opentelemetry/auto-instrumentations-node": "^0.55.0",
|
|
28
|
+
"@opentelemetry/sdk-trace-base": "^1.30.0",
|
|
21
29
|
"@opentelemetry/resources": "^1.30.0",
|
|
22
30
|
"@opentelemetry/semantic-conventions": "^1.30.0"
|
|
23
31
|
},
|
|
24
32
|
"devDependencies": {
|
|
25
33
|
"vitest": "^2.1.0",
|
|
26
34
|
"typescript": "^5.7.0",
|
|
27
|
-
"@types/node": "^22.0.0"
|
|
35
|
+
"@types/node": "^22.0.0",
|
|
36
|
+
"@cloudflare/workers-types": "^4"
|
|
28
37
|
}
|
|
29
38
|
}
|
package/dist/index.d.ts
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import { NodeSDK } from "@opentelemetry/sdk-node";
|
|
2
|
-
export interface HeystackOptions {
|
|
3
|
-
/** Your Heystack ingest key (sk_live_...). */
|
|
4
|
-
apiKey: string;
|
|
5
|
-
/** The OTel service.name this app reports as (must match the app's service in Heystack). */
|
|
6
|
-
service: string;
|
|
7
|
-
/** Override the ingest endpoint (defaults to Heystack production). */
|
|
8
|
-
endpoint?: string;
|
|
9
|
-
}
|
|
10
|
-
export interface ExporterConfig {
|
|
11
|
-
url: string;
|
|
12
|
-
headers: {
|
|
13
|
-
Authorization: string;
|
|
14
|
-
};
|
|
15
|
-
}
|
|
16
|
-
/** Pure: derive the OTLP/HTTP exporter URL + auth header. Unit-tested. */
|
|
17
|
-
export declare function buildExporterConfig(o: HeystackOptions): ExporterConfig;
|
|
18
|
-
/**
|
|
19
|
-
* Initialise Heystack OpenTelemetry tracing. Call once, as early as possible in
|
|
20
|
-
* your app's entry point. Returns the started NodeSDK.
|
|
21
|
-
*/
|
|
22
|
-
export declare function initHeystack(o: HeystackOptions): NodeSDK;
|
package/dist/index.js
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import { NodeSDK } from "@opentelemetry/sdk-node";
|
|
2
|
-
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
|
|
3
|
-
const DEFAULT_ENDPOINT = "https://ingest.heystack.dev";
|
|
4
|
-
/** Pure: derive the OTLP/HTTP exporter URL + auth header. Unit-tested. */
|
|
5
|
-
export function buildExporterConfig(o) {
|
|
6
|
-
const base = (o.endpoint ?? DEFAULT_ENDPOINT).replace(/\/+$/, "");
|
|
7
|
-
return { url: `${base}/v1/traces`, headers: { Authorization: `Bearer ${o.apiKey}` } };
|
|
8
|
-
}
|
|
9
|
-
/**
|
|
10
|
-
* Initialise Heystack OpenTelemetry tracing. Call once, as early as possible in
|
|
11
|
-
* your app's entry point. Returns the started NodeSDK.
|
|
12
|
-
*/
|
|
13
|
-
export function initHeystack(o) {
|
|
14
|
-
const cfg = buildExporterConfig(o);
|
|
15
|
-
const sdk = new NodeSDK({
|
|
16
|
-
serviceName: o.service,
|
|
17
|
-
traceExporter: new OTLPTraceExporter({ url: cfg.url, headers: cfg.headers }),
|
|
18
|
-
});
|
|
19
|
-
sdk.start();
|
|
20
|
-
return sdk;
|
|
21
|
-
}
|