@braintrust/temporal 0.1.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 ADDED
@@ -0,0 +1,54 @@
1
+ # @braintrust/temporal
2
+
3
+ SDK for integrating [Braintrust](https://braintrust.dev) tracing with [Temporal](https://temporal.io/) workflows and activities.
4
+
5
+ ## Installation
6
+
7
+ This package has peer dependencies that you must install alongside it:
8
+
9
+ ```bash
10
+ npm install @braintrust/temporal braintrust @temporalio/client @temporalio/worker @temporalio/workflow @temporalio/activity @temporalio/common
11
+ # or
12
+ yarn add @braintrust/temporal braintrust @temporalio/client @temporalio/worker @temporalio/workflow @temporalio/activity @temporalio/common
13
+ # or
14
+ pnpm add @braintrust/temporal braintrust @temporalio/client @temporalio/worker @temporalio/workflow @temporalio/activity @temporalio/common
15
+ ```
16
+
17
+ ## Usage
18
+
19
+ Initialize Braintrust, then install the plugin on both the Temporal client and worker.
20
+
21
+ ```typescript
22
+ import { Client, Connection } from "@temporalio/client";
23
+ import { Worker } from "@temporalio/worker";
24
+ import * as braintrust from "braintrust";
25
+ import { BraintrustTemporalPlugin } from "@braintrust/temporal";
26
+
27
+ braintrust.initLogger({ projectName: "my-project" });
28
+
29
+ const plugin = new BraintrustTemporalPlugin();
30
+
31
+ const client = new Client({
32
+ connection: await Connection.connect(),
33
+ plugins: [plugin],
34
+ });
35
+
36
+ const worker = await Worker.create({
37
+ taskQueue: "my-queue",
38
+ workflowsPath: require.resolve("./workflows"),
39
+ activities,
40
+ plugins: [plugin],
41
+ });
42
+ ```
43
+
44
+ ## Workflow interceptors
45
+
46
+ This package also exports workflow interceptors that are loaded into the Temporal workflow isolate:
47
+
48
+ - `@braintrust/temporal/workflow-interceptors`
49
+
50
+ The `BraintrustTemporalPlugin` automatically configures `workflowModules` to include these interceptors when used on a worker.
51
+
52
+ ## Example
53
+
54
+ See the example app in `examples/temporal`.
@@ -0,0 +1,39 @@
1
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
2
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
3
+ }) : x)(function(x) {
4
+ if (typeof require !== "undefined") return require.apply(this, arguments);
5
+ throw Error('Dynamic require of "' + x + '" is not supported');
6
+ });
7
+
8
+ // src/utils.ts
9
+ var BRAINTRUST_SPAN_HEADER = "_braintrust-span";
10
+ var BRAINTRUST_WORKFLOW_SPAN_HEADER = "_braintrust-workflow-span";
11
+ var BRAINTRUST_WORKFLOW_SPAN_ID_HEADER = "_braintrust-workflow-span-id";
12
+ function serializeHeaderValue(value) {
13
+ return {
14
+ metadata: {
15
+ encoding: new TextEncoder().encode("json/plain")
16
+ },
17
+ data: new TextEncoder().encode(JSON.stringify(value))
18
+ };
19
+ }
20
+ function deserializeHeaderValue(payload) {
21
+ if (!payload?.data) {
22
+ return void 0;
23
+ }
24
+ try {
25
+ const decoded = new TextDecoder().decode(payload.data);
26
+ return JSON.parse(decoded);
27
+ } catch {
28
+ return void 0;
29
+ }
30
+ }
31
+
32
+ export {
33
+ __require,
34
+ BRAINTRUST_SPAN_HEADER,
35
+ BRAINTRUST_WORKFLOW_SPAN_HEADER,
36
+ BRAINTRUST_WORKFLOW_SPAN_ID_HEADER,
37
+ serializeHeaderValue,
38
+ deserializeHeaderValue
39
+ };
@@ -0,0 +1,71 @@
1
+ import { ClientPlugin, ClientOptions } from '@temporalio/client';
2
+ import { WorkerPlugin, WorkerOptions } from '@temporalio/worker';
3
+ import { Sinks } from '@temporalio/workflow';
4
+
5
+ /**
6
+ * A Braintrust plugin for Temporal that automatically instruments
7
+ * workflows and activities with tracing spans.
8
+ *
9
+ * This plugin implements both ClientPlugin and WorkerPlugin interfaces,
10
+ * so it can be used with both Temporal Client and Worker.
11
+ *
12
+ * @example
13
+ * ```typescript
14
+ * import { Client, Connection } from "@temporalio/client";
15
+ * import { Worker } from "@temporalio/worker";
16
+ * import * as braintrust from "braintrust";
17
+ * import { BraintrustTemporalPlugin } from "@braintrust/temporal";
18
+ *
19
+ * // Initialize Braintrust logger
20
+ * braintrust.initLogger({ projectName: "my-project" });
21
+ *
22
+ * // Create client with the plugin
23
+ * const client = new Client({
24
+ * connection: await Connection.connect(),
25
+ * plugins: [new BraintrustTemporalPlugin()],
26
+ * });
27
+ *
28
+ * // Create worker with the plugin
29
+ * const worker = await Worker.create({
30
+ * taskQueue: "my-queue",
31
+ * workflowsPath: require.resolve("./workflows"),
32
+ * activities,
33
+ * plugins: [new BraintrustTemporalPlugin()],
34
+ * });
35
+ * ```
36
+ */
37
+ declare class BraintrustTemporalPlugin implements ClientPlugin, WorkerPlugin {
38
+ get name(): string;
39
+ /**
40
+ * Configure the Temporal Client with Braintrust interceptors.
41
+ * Adds the client interceptor for propagating span context to workflows.
42
+ */
43
+ configureClient(options: Omit<ClientOptions, "plugins">): Omit<ClientOptions, "plugins">;
44
+ /**
45
+ * Configure the Temporal Worker with Braintrust interceptors and sinks.
46
+ * Adds the activity interceptor for creating spans, the sinks for workflow spans,
47
+ * and the workflow interceptor modules for bundling.
48
+ */
49
+ configureWorker(options: WorkerOptions): WorkerOptions;
50
+ }
51
+ /**
52
+ * Create a Braintrust plugin for Temporal.
53
+ *
54
+ * @example
55
+ * ```typescript
56
+ * const plugin = createBraintrustTemporalPlugin();
57
+ *
58
+ * const client = new Client({ plugins: [plugin] });
59
+ * const worker = await Worker.create({ plugins: [plugin], ... });
60
+ * ```
61
+ */
62
+ declare function createBraintrustTemporalPlugin(): BraintrustTemporalPlugin;
63
+
64
+ interface BraintrustSinks extends Sinks {
65
+ braintrust: {
66
+ workflowStarted(parentContext?: string, workflowSpanId?: string): void;
67
+ workflowCompleted(error?: string): void;
68
+ };
69
+ }
70
+
71
+ export { type BraintrustSinks, BraintrustTemporalPlugin, createBraintrustTemporalPlugin };
@@ -0,0 +1,71 @@
1
+ import { ClientPlugin, ClientOptions } from '@temporalio/client';
2
+ import { WorkerPlugin, WorkerOptions } from '@temporalio/worker';
3
+ import { Sinks } from '@temporalio/workflow';
4
+
5
+ /**
6
+ * A Braintrust plugin for Temporal that automatically instruments
7
+ * workflows and activities with tracing spans.
8
+ *
9
+ * This plugin implements both ClientPlugin and WorkerPlugin interfaces,
10
+ * so it can be used with both Temporal Client and Worker.
11
+ *
12
+ * @example
13
+ * ```typescript
14
+ * import { Client, Connection } from "@temporalio/client";
15
+ * import { Worker } from "@temporalio/worker";
16
+ * import * as braintrust from "braintrust";
17
+ * import { BraintrustTemporalPlugin } from "@braintrust/temporal";
18
+ *
19
+ * // Initialize Braintrust logger
20
+ * braintrust.initLogger({ projectName: "my-project" });
21
+ *
22
+ * // Create client with the plugin
23
+ * const client = new Client({
24
+ * connection: await Connection.connect(),
25
+ * plugins: [new BraintrustTemporalPlugin()],
26
+ * });
27
+ *
28
+ * // Create worker with the plugin
29
+ * const worker = await Worker.create({
30
+ * taskQueue: "my-queue",
31
+ * workflowsPath: require.resolve("./workflows"),
32
+ * activities,
33
+ * plugins: [new BraintrustTemporalPlugin()],
34
+ * });
35
+ * ```
36
+ */
37
+ declare class BraintrustTemporalPlugin implements ClientPlugin, WorkerPlugin {
38
+ get name(): string;
39
+ /**
40
+ * Configure the Temporal Client with Braintrust interceptors.
41
+ * Adds the client interceptor for propagating span context to workflows.
42
+ */
43
+ configureClient(options: Omit<ClientOptions, "plugins">): Omit<ClientOptions, "plugins">;
44
+ /**
45
+ * Configure the Temporal Worker with Braintrust interceptors and sinks.
46
+ * Adds the activity interceptor for creating spans, the sinks for workflow spans,
47
+ * and the workflow interceptor modules for bundling.
48
+ */
49
+ configureWorker(options: WorkerOptions): WorkerOptions;
50
+ }
51
+ /**
52
+ * Create a Braintrust plugin for Temporal.
53
+ *
54
+ * @example
55
+ * ```typescript
56
+ * const plugin = createBraintrustTemporalPlugin();
57
+ *
58
+ * const client = new Client({ plugins: [plugin] });
59
+ * const worker = await Worker.create({ plugins: [plugin], ... });
60
+ * ```
61
+ */
62
+ declare function createBraintrustTemporalPlugin(): BraintrustTemporalPlugin;
63
+
64
+ interface BraintrustSinks extends Sinks {
65
+ braintrust: {
66
+ workflowStarted(parentContext?: string, workflowSpanId?: string): void;
67
+ workflowCompleted(error?: string): void;
68
+ };
69
+ }
70
+
71
+ export { type BraintrustSinks, BraintrustTemporalPlugin, createBraintrustTemporalPlugin };
package/dist/index.js ADDED
@@ -0,0 +1,301 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
+ BraintrustTemporalPlugin: () => BraintrustTemporalPlugin,
34
+ createBraintrustTemporalPlugin: () => createBraintrustTemporalPlugin
35
+ });
36
+ module.exports = __toCommonJS(index_exports);
37
+
38
+ // src/interceptors.ts
39
+ var import_common = require("@temporalio/common");
40
+ var braintrust2 = __toESM(require("braintrust"));
41
+ var import_util = require("braintrust/util");
42
+
43
+ // src/sinks.ts
44
+ var braintrust = __toESM(require("braintrust"));
45
+ var workflowSpans = /* @__PURE__ */ new Map();
46
+ var workflowSpanExports = /* @__PURE__ */ new Map();
47
+ function getWorkflowSpanExport(runId) {
48
+ return workflowSpanExports.get(runId);
49
+ }
50
+ function createBraintrustSinks() {
51
+ return {
52
+ braintrust: {
53
+ workflowStarted: {
54
+ fn: (info, parentContext, workflowSpanId) => {
55
+ const span = braintrust.startSpan({
56
+ name: `temporal.workflow.${info.workflowType}`,
57
+ spanAttributes: { type: "task" },
58
+ parent: parentContext,
59
+ spanId: workflowSpanId,
60
+ event: {
61
+ metadata: {
62
+ "temporal.workflow_type": info.workflowType,
63
+ "temporal.workflow_id": info.workflowId,
64
+ "temporal.run_id": info.runId
65
+ }
66
+ }
67
+ });
68
+ workflowSpans.set(info.runId, span);
69
+ workflowSpanExports.set(info.runId, span.export());
70
+ },
71
+ callDuringReplay: false
72
+ },
73
+ workflowCompleted: {
74
+ fn: (info, error) => {
75
+ const span = workflowSpans.get(info.runId);
76
+ if (span) {
77
+ if (error) {
78
+ span.log({ error });
79
+ }
80
+ span.end();
81
+ workflowSpans.delete(info.runId);
82
+ workflowSpanExports.delete(info.runId);
83
+ }
84
+ },
85
+ callDuringReplay: false
86
+ }
87
+ }
88
+ };
89
+ }
90
+
91
+ // src/utils.ts
92
+ var BRAINTRUST_SPAN_HEADER = "_braintrust-span";
93
+ var BRAINTRUST_WORKFLOW_SPAN_ID_HEADER = "_braintrust-workflow-span-id";
94
+ function deserializeHeaderValue(payload) {
95
+ if (!payload?.data) {
96
+ return void 0;
97
+ }
98
+ try {
99
+ const decoded = new TextDecoder().decode(payload.data);
100
+ return JSON.parse(decoded);
101
+ } catch {
102
+ return void 0;
103
+ }
104
+ }
105
+
106
+ // src/interceptors.ts
107
+ function createBraintrustClientInterceptor() {
108
+ return {
109
+ async start(input, next) {
110
+ const span = braintrust2.currentSpan();
111
+ if (span) {
112
+ const exported = await span.export();
113
+ if (exported) {
114
+ const payload = import_common.defaultPayloadConverter.toPayload(exported);
115
+ if (payload) {
116
+ return next({
117
+ ...input,
118
+ headers: {
119
+ ...input.headers,
120
+ [BRAINTRUST_SPAN_HEADER]: payload
121
+ }
122
+ });
123
+ }
124
+ }
125
+ }
126
+ return next(input);
127
+ },
128
+ async signal(input, next) {
129
+ return next(input);
130
+ },
131
+ async signalWithStart(input, next) {
132
+ const span = braintrust2.currentSpan();
133
+ if (span) {
134
+ const exported = await span.export();
135
+ if (exported) {
136
+ const payload = import_common.defaultPayloadConverter.toPayload(exported);
137
+ if (payload) {
138
+ return next({
139
+ ...input,
140
+ headers: {
141
+ ...input.headers,
142
+ [BRAINTRUST_SPAN_HEADER]: payload
143
+ }
144
+ });
145
+ }
146
+ }
147
+ }
148
+ return next(input);
149
+ }
150
+ };
151
+ }
152
+ var BraintrustActivityInterceptor = class {
153
+ constructor(ctx) {
154
+ this.ctx = ctx;
155
+ }
156
+ async execute(input, next) {
157
+ const info = this.ctx.info;
158
+ const runId = info.workflowExecution.runId;
159
+ let parent;
160
+ const spanExportPromise = getWorkflowSpanExport(runId);
161
+ if (spanExportPromise) {
162
+ try {
163
+ parent = await spanExportPromise;
164
+ } catch {
165
+ }
166
+ }
167
+ if (!parent && input.headers) {
168
+ const workflowSpanId = deserializeHeaderValue(
169
+ input.headers[BRAINTRUST_WORKFLOW_SPAN_ID_HEADER]
170
+ );
171
+ const clientContext = deserializeHeaderValue(
172
+ input.headers[BRAINTRUST_SPAN_HEADER]
173
+ );
174
+ if (workflowSpanId && clientContext) {
175
+ try {
176
+ const clientComponents = import_util.SpanComponentsV3.fromStr(clientContext);
177
+ const clientData = clientComponents.data;
178
+ const hasTracingContext = !!clientData.root_span_id;
179
+ const hasObjectMetadata = !!clientData.object_id || !!clientData.compute_object_metadata_args;
180
+ if (hasTracingContext && hasObjectMetadata) {
181
+ const workflowComponents = new import_util.SpanComponentsV3({
182
+ object_type: clientData.object_type,
183
+ object_id: clientData.object_id || void 0,
184
+ compute_object_metadata_args: clientData.object_id ? void 0 : clientData.compute_object_metadata_args || void 0,
185
+ propagated_event: clientData.propagated_event,
186
+ row_id: workflowSpanId,
187
+ // Use workflow's row_id, not client's
188
+ span_id: workflowSpanId,
189
+ // Use workflow's span_id, not client's
190
+ root_span_id: clientData.root_span_id
191
+ // Keep same trace
192
+ });
193
+ parent = workflowComponents.toStr();
194
+ } else {
195
+ parent = clientContext;
196
+ }
197
+ } catch {
198
+ parent = clientContext;
199
+ }
200
+ } else if (clientContext) {
201
+ parent = clientContext;
202
+ }
203
+ }
204
+ const span = braintrust2.startSpan({
205
+ name: `temporal.activity.${info.activityType}`,
206
+ spanAttributes: { type: "task" },
207
+ parent,
208
+ event: {
209
+ metadata: {
210
+ "temporal.activity_type": info.activityType,
211
+ "temporal.activity_id": info.activityId,
212
+ "temporal.workflow_id": info.workflowExecution.workflowId,
213
+ "temporal.workflow_run_id": runId
214
+ }
215
+ }
216
+ });
217
+ try {
218
+ const result = await braintrust2.withCurrent(span, () => next(input));
219
+ span.log({ output: result });
220
+ span.end();
221
+ return result;
222
+ } catch (e) {
223
+ span.log({ error: String(e) });
224
+ span.end();
225
+ throw e;
226
+ }
227
+ }
228
+ };
229
+ function createBraintrustActivityInterceptor(ctx) {
230
+ return {
231
+ inbound: new BraintrustActivityInterceptor(ctx)
232
+ };
233
+ }
234
+
235
+ // src/plugin.ts
236
+ var BraintrustTemporalPlugin = class {
237
+ get name() {
238
+ return "braintrust";
239
+ }
240
+ /**
241
+ * Configure the Temporal Client with Braintrust interceptors.
242
+ * Adds the client interceptor for propagating span context to workflows.
243
+ */
244
+ configureClient(options) {
245
+ const existing = options.interceptors?.workflow;
246
+ const braintrustInterceptor = createBraintrustClientInterceptor();
247
+ let workflow;
248
+ if (Array.isArray(existing)) {
249
+ workflow = [...existing, braintrustInterceptor];
250
+ } else if (existing) {
251
+ workflow = {
252
+ ...existing,
253
+ ...braintrustInterceptor
254
+ };
255
+ } else {
256
+ workflow = [braintrustInterceptor];
257
+ }
258
+ return {
259
+ ...options,
260
+ interceptors: {
261
+ ...options.interceptors,
262
+ workflow
263
+ }
264
+ };
265
+ }
266
+ /**
267
+ * Configure the Temporal Worker with Braintrust interceptors and sinks.
268
+ * Adds the activity interceptor for creating spans, the sinks for workflow spans,
269
+ * and the workflow interceptor modules for bundling.
270
+ */
271
+ configureWorker(options) {
272
+ const existingActivityInterceptors = options.interceptors?.activity ?? [];
273
+ const existingWorkflowModules = options.interceptors?.workflowModules ?? [];
274
+ const existingSinks = options.sinks ?? {};
275
+ const braintrustSinks = createBraintrustSinks();
276
+ const workflowInterceptorsPath = require.resolve("@braintrust/temporal/workflow-interceptors");
277
+ return {
278
+ ...options,
279
+ interceptors: {
280
+ ...options.interceptors,
281
+ activity: [
282
+ ...existingActivityInterceptors,
283
+ createBraintrustActivityInterceptor
284
+ ],
285
+ workflowModules: [...existingWorkflowModules, workflowInterceptorsPath]
286
+ },
287
+ sinks: {
288
+ ...existingSinks,
289
+ ...braintrustSinks
290
+ }
291
+ };
292
+ }
293
+ };
294
+ function createBraintrustTemporalPlugin() {
295
+ return new BraintrustTemporalPlugin();
296
+ }
297
+ // Annotate the CommonJS export names for ESM import in node:
298
+ 0 && (module.exports = {
299
+ BraintrustTemporalPlugin,
300
+ createBraintrustTemporalPlugin
301
+ });