mirror of
https://github.com/zoriya/drizzle-otel.git
synced 2025-12-06 00:46:09 +00:00
Merge pull request #28 from kubiks-inc/kubiks-inc/e2b
Add OpenTelemetry instrumentation for E2B Sandboxes (@kubiks/otel-e2b)
This commit is contained in:
@@ -15,6 +15,7 @@ Our goal is to bring the TypeScript ecosystem the observability tools it’s bee
|
||||
- [`@kubiks/otel-autumn`](./packages/otel-autumn/README.md)
|
||||
- [`@kubiks/otel-better-auth`](./packages/otel-better-auth/README.md)
|
||||
- [`@kubiks/otel-drizzle`](./packages/otel-drizzle/README.md)
|
||||
- [`@kubiks/otel-e2b`](./packages/otel-e2b/README.md)
|
||||
- [`@kubiks/otel-inbound`](./packages/otel-inbound/README.md)
|
||||
- [`@kubiks/otel-mongodb`](./packages/otel-mongodb/README.md)
|
||||
- [`@kubiks/otel-resend`](./packages/otel-resend/README.md)
|
||||
|
||||
BIN
images/otel-e2b-trace.png
Normal file
BIN
images/otel-e2b-trace.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.5 MiB |
28
packages/otel-e2b/CHANGELOG.md
Normal file
28
packages/otel-e2b/CHANGELOG.md
Normal file
@@ -0,0 +1,28 @@
|
||||
# @kubiks/otel-e2b
|
||||
|
||||
## 1.0.0
|
||||
|
||||
### Features
|
||||
|
||||
- Initial release of OpenTelemetry instrumentation for E2B Sandboxes
|
||||
- **Core instrumentation functions**:
|
||||
- `instrumentSandbox()` - Instrument existing sandbox instances
|
||||
- `instrumentSandboxClass()` - Instrument Sandbox class for automatic instrumentation
|
||||
- **Sandbox lifecycle tracing**:
|
||||
- `Sandbox.create()` - Sandbox creation with template information
|
||||
- `sandbox.kill()` - Sandbox termination
|
||||
- **Code execution tracing**:
|
||||
- `sandbox.runCode()` - Code execution with language, error status, and execution count
|
||||
- **File operations tracing**:
|
||||
- `sandbox.files.read()` - Read files with path, size, and format
|
||||
- `sandbox.files.write()` - Write single or multiple files
|
||||
- `sandbox.files.list()` - List directory contents with file counts
|
||||
- `sandbox.files.remove()` - Delete files or directories
|
||||
- `sandbox.files.makeDir()` - Create directories
|
||||
- **Command execution tracing**:
|
||||
- `sandbox.commands.run()` - Execute shell commands with exit codes and output line counts
|
||||
- **Security-first design**: Never captures sensitive data (code content, file contents, command arguments, etc.)
|
||||
- **Comprehensive configuration options**: Control what metadata to capture
|
||||
- **Idempotent instrumentation**: Safe to call multiple times
|
||||
- **Full TypeScript support** with complete type definitions
|
||||
- **OpenTelemetry semantic conventions** for all span attributes
|
||||
22
packages/otel-e2b/LICENSE
Normal file
22
packages/otel-e2b/LICENSE
Normal file
@@ -0,0 +1,22 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Kubiks
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
85
packages/otel-e2b/README.md
Normal file
85
packages/otel-e2b/README.md
Normal file
@@ -0,0 +1,85 @@
|
||||
# @kubiks/otel-e2b
|
||||
|
||||
OpenTelemetry instrumentation for [E2B Sandboxes](https://e2b.dev).
|
||||
Capture spans for sandbox lifecycle, code execution, file operations, and command execution to monitor and debug your E2B sandbox operations.
|
||||
|
||||

|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
npm install @kubiks/otel-e2b
|
||||
# or
|
||||
pnpm add @kubiks/otel-e2b
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
```ts
|
||||
import { Sandbox } from "@e2b/code-interpreter";
|
||||
import { instrumentSandbox } from "@kubiks/otel-e2b";
|
||||
|
||||
const sandbox = await Sandbox.create();
|
||||
instrumentSandbox(sandbox);
|
||||
|
||||
// All operations are now traced
|
||||
await sandbox.runCode('print("Hello from E2B")');
|
||||
await sandbox.files.write("/app/data.txt", "some data");
|
||||
await sandbox.commands.run("ls -la");
|
||||
await sandbox.kill();
|
||||
```
|
||||
|
||||
`instrumentSandbox` wraps the sandbox you already use — no configuration changes needed. Every operation creates a client span with useful attributes.
|
||||
|
||||
You can also use `instrumentSandboxClass(Sandbox)` to automatically instrument all sandboxes created after setup.
|
||||
|
||||
## What Gets Traced
|
||||
|
||||
This instrumentation automatically traces all E2B sandbox operations including `Sandbox.create()`, `sandbox.kill()`, `sandbox.runCode()` (code execution), `sandbox.commands.run()` (shell commands), and all file operations (`files.read()`, `files.write()`, `files.list()`, `files.remove()`, `files.makeDir()`).
|
||||
|
||||
## Span Attributes
|
||||
|
||||
Each span includes relevant attributes for debugging and monitoring:
|
||||
|
||||
| Attribute | Description | Example |
|
||||
| -------------------------- | ------------------------------------- | ---------------------------- |
|
||||
| `e2b.operation` | Operation type | `sandbox.create`, `code.run` |
|
||||
| `e2b.sandbox.id` | Unique sandbox identifier | `sb_abc123def456` |
|
||||
| `e2b.sandbox.template` | Template used for creation | `custom-template` |
|
||||
| `e2b.code.language` | Programming language | `python`, `javascript` |
|
||||
| `e2b.code.has_error` | Whether execution had errors | `true`, `false` |
|
||||
| `e2b.code.execution_count` | Execution count from result | `1`, `2`, `3` |
|
||||
| `e2b.command.exit_code` | Process exit code | `0`, `1`, `127` |
|
||||
| `e2b.command.stdout_lines` | Number of stdout lines (when enabled) | `5` |
|
||||
| `e2b.command.stderr_lines` | Number of stderr lines (when enabled) | `2` |
|
||||
| `e2b.command.background` | Whether command ran in background | `true`, `false` |
|
||||
| `e2b.file.operation` | File operation type | `read`, `write`, `list` |
|
||||
| `e2b.file.path` | File or directory path | `/app/data.txt` |
|
||||
| `e2b.file.size_bytes` | File size in bytes | `1024` |
|
||||
| `e2b.file.format` | Read format (for read ops) | `text`, `bytes` |
|
||||
| `e2b.file.count` | Number of files (list/write multiple) | `10` |
|
||||
|
||||
## Configuration Options
|
||||
|
||||
The instrumentation accepts optional configuration to control what metadata to capture:
|
||||
|
||||
- `tracerName` - Custom tracer name (default: `"@kubiks/otel-e2b"`)
|
||||
- `captureFilePaths` - Capture file paths, not content (default: `true`)
|
||||
- `captureFileSize` - Capture file sizes (default: `true`)
|
||||
- `captureCodeLanguage` - Capture code execution language (default: `true`)
|
||||
- `captureCommandOutput` - Capture command output line counts, not content (default: `false`)
|
||||
|
||||
Example:
|
||||
|
||||
```ts
|
||||
instrumentSandbox(sandbox, {
|
||||
captureFilePaths: true,
|
||||
captureCommandOutput: true,
|
||||
});
|
||||
```
|
||||
|
||||
The instrumentation never captures sensitive data like code content, command arguments, file contents, or environment variables — only safe metadata like paths, sizes, exit codes, and language types.
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
65
packages/otel-e2b/package.json
Normal file
65
packages/otel-e2b/package.json
Normal file
@@ -0,0 +1,65 @@
|
||||
{
|
||||
"name": "@kubiks/otel-e2b",
|
||||
"version": "1.0.0",
|
||||
"private": false,
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"description": "OpenTelemetry instrumentation for E2B Sandboxes",
|
||||
"keywords": [
|
||||
"opentelemetry",
|
||||
"otel",
|
||||
"instrumentation",
|
||||
"e2b",
|
||||
"sandbox",
|
||||
"code-interpreter",
|
||||
"observability",
|
||||
"tracing",
|
||||
"monitoring",
|
||||
"telemetry"
|
||||
],
|
||||
"author": "Kubiks",
|
||||
"license": "MIT",
|
||||
"repository": "kubiks-inc/otel",
|
||||
"sideEffects": false,
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/types/index.d.ts",
|
||||
"import": "./dist/index.js",
|
||||
"default": "./dist/index.js"
|
||||
}
|
||||
},
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/types/index.d.ts",
|
||||
"files": [
|
||||
"dist",
|
||||
"LICENSE",
|
||||
"README.md"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.19.0 || >=20.6.0"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "pnpm clean && tsc",
|
||||
"clean": "rimraf dist",
|
||||
"prepublishOnly": "pnpm build",
|
||||
"type-check": "tsc --noEmit",
|
||||
"unit-test": "vitest --run",
|
||||
"unit-test-watch": "vitest"
|
||||
},
|
||||
"dependencies": {},
|
||||
"devDependencies": {
|
||||
"@opentelemetry/api": "^1.9.0",
|
||||
"@opentelemetry/sdk-trace-base": "^2.1.0",
|
||||
"@e2b/code-interpreter": "^2.0.0",
|
||||
"@types/node": "18.15.11",
|
||||
"rimraf": "3.0.2",
|
||||
"typescript": "^5",
|
||||
"vitest": "0.33.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@opentelemetry/api": ">=1.9.0 <2.0.0",
|
||||
"@e2b/code-interpreter": ">=2.0.0"
|
||||
}
|
||||
}
|
||||
505
packages/otel-e2b/src/index.test.ts
Normal file
505
packages/otel-e2b/src/index.test.ts
Normal file
@@ -0,0 +1,505 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { SpanStatusCode, trace } from "@opentelemetry/api";
|
||||
import {
|
||||
BasicTracerProvider,
|
||||
InMemorySpanExporter,
|
||||
SimpleSpanProcessor,
|
||||
} from "@opentelemetry/sdk-trace-base";
|
||||
import type { Sandbox } from "@e2b/code-interpreter";
|
||||
import {
|
||||
instrumentSandbox,
|
||||
instrumentSandboxClass,
|
||||
SEMATTRS_E2B_OPERATION,
|
||||
SEMATTRS_E2B_SANDBOX_ID,
|
||||
SEMATTRS_E2B_CODE_LANGUAGE,
|
||||
SEMATTRS_E2B_CODE_HAS_ERROR,
|
||||
SEMATTRS_E2B_CODE_EXECUTION_COUNT,
|
||||
SEMATTRS_E2B_COMMAND_EXIT_CODE,
|
||||
SEMATTRS_E2B_COMMAND_STDOUT_LINES,
|
||||
SEMATTRS_E2B_COMMAND_STDERR_LINES,
|
||||
SEMATTRS_E2B_COMMAND_BACKGROUND,
|
||||
SEMATTRS_E2B_FILE_OPERATION,
|
||||
SEMATTRS_E2B_FILE_PATH,
|
||||
SEMATTRS_E2B_FILE_SIZE_BYTES,
|
||||
SEMATTRS_E2B_FILE_COUNT,
|
||||
SEMATTRS_E2B_FILE_FORMAT,
|
||||
SEMATTRS_E2B_SANDBOX_TEMPLATE,
|
||||
} from "./index";
|
||||
|
||||
describe("instrumentSandbox", () => {
|
||||
let provider: BasicTracerProvider;
|
||||
let exporter: InMemorySpanExporter;
|
||||
|
||||
beforeEach(() => {
|
||||
exporter = new InMemorySpanExporter();
|
||||
provider = new BasicTracerProvider({
|
||||
spanProcessors: [new SimpleSpanProcessor(exporter)],
|
||||
});
|
||||
trace.setGlobalTracerProvider(provider);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await provider.shutdown();
|
||||
exporter.reset();
|
||||
trace.disable();
|
||||
});
|
||||
|
||||
const createMockSandbox = (): Sandbox => {
|
||||
const mockSandbox = {
|
||||
sandboxId: "test-sandbox-123",
|
||||
kill: vi.fn(async () => {}),
|
||||
runCode: vi.fn(async () => ({
|
||||
results: [],
|
||||
logs: { stdout: [], stderr: [] },
|
||||
error: undefined,
|
||||
executionCount: 1,
|
||||
})),
|
||||
files: {
|
||||
read: vi.fn(async () => "file content"),
|
||||
write: vi.fn(async () => ({
|
||||
path: "/test/file.txt",
|
||||
name: "file.txt",
|
||||
})),
|
||||
list: vi.fn(async () => [
|
||||
{ name: "file1.txt", type: "file" },
|
||||
{ name: "file2.txt", type: "file" },
|
||||
]),
|
||||
remove: vi.fn(async () => {}),
|
||||
makeDir: vi.fn(async () => true),
|
||||
},
|
||||
commands: {
|
||||
run: vi.fn(async () => ({
|
||||
exitCode: 0,
|
||||
stdout: ["output line 1", "output line 2"],
|
||||
stderr: [],
|
||||
})),
|
||||
},
|
||||
} as unknown as Sandbox;
|
||||
|
||||
return mockSandbox;
|
||||
};
|
||||
|
||||
it("instruments sandbox.kill() and records spans", async () => {
|
||||
const sandbox = createMockSandbox();
|
||||
instrumentSandbox(sandbox);
|
||||
|
||||
await sandbox.kill();
|
||||
|
||||
const spans = exporter.getFinishedSpans();
|
||||
expect(spans).toHaveLength(1);
|
||||
|
||||
const span = spans[0];
|
||||
expect(span?.name).toBe("e2b.sandbox.kill");
|
||||
expect(span?.attributes[SEMATTRS_E2B_OPERATION]).toBe("sandbox.kill");
|
||||
expect(span?.attributes[SEMATTRS_E2B_SANDBOX_ID]).toBe("test-sandbox-123");
|
||||
expect(span?.status.code).toBe(SpanStatusCode.OK);
|
||||
});
|
||||
|
||||
it("instruments sandbox.runCode() and captures execution details", async () => {
|
||||
const sandbox = createMockSandbox();
|
||||
instrumentSandbox(sandbox);
|
||||
|
||||
const result = await (sandbox as any).runCode('print("hello")', {
|
||||
language: "python",
|
||||
});
|
||||
|
||||
expect(result.executionCount).toBe(1);
|
||||
|
||||
const spans = exporter.getFinishedSpans();
|
||||
expect(spans).toHaveLength(1);
|
||||
|
||||
const span = spans[0];
|
||||
expect(span?.name).toBe("e2b.code.run");
|
||||
expect(span?.attributes[SEMATTRS_E2B_OPERATION]).toBe("code.run");
|
||||
expect(span?.attributes[SEMATTRS_E2B_SANDBOX_ID]).toBe("test-sandbox-123");
|
||||
expect(span?.attributes[SEMATTRS_E2B_CODE_LANGUAGE]).toBe("python");
|
||||
expect(span?.attributes[SEMATTRS_E2B_CODE_HAS_ERROR]).toBe(false);
|
||||
expect(span?.attributes[SEMATTRS_E2B_CODE_EXECUTION_COUNT]).toBe(1);
|
||||
expect(span?.status.code).toBe(SpanStatusCode.OK);
|
||||
});
|
||||
|
||||
it("instruments sandbox.files.read() and captures file details", async () => {
|
||||
const sandbox = createMockSandbox();
|
||||
instrumentSandbox(sandbox);
|
||||
|
||||
const content = await sandbox.files.read("/path/to/file.txt");
|
||||
|
||||
expect(content).toBe("file content");
|
||||
|
||||
const spans = exporter.getFinishedSpans();
|
||||
expect(spans).toHaveLength(1);
|
||||
|
||||
const span = spans[0];
|
||||
expect(span?.name).toBe("e2b.files.read");
|
||||
expect(span?.attributes[SEMATTRS_E2B_OPERATION]).toBe("files.read");
|
||||
expect(span?.attributes[SEMATTRS_E2B_FILE_OPERATION]).toBe("read");
|
||||
expect(span?.attributes[SEMATTRS_E2B_FILE_PATH]).toBe("/path/to/file.txt");
|
||||
expect(span?.attributes[SEMATTRS_E2B_FILE_SIZE_BYTES]).toBe(12); // "file content".length
|
||||
expect(span?.status.code).toBe(SpanStatusCode.OK);
|
||||
});
|
||||
|
||||
it("instruments sandbox.files.read() with format option", async () => {
|
||||
const sandbox = createMockSandbox();
|
||||
instrumentSandbox(sandbox);
|
||||
|
||||
await sandbox.files.read("/path/to/file.txt", { format: "bytes" });
|
||||
|
||||
const spans = exporter.getFinishedSpans();
|
||||
expect(spans).toHaveLength(1);
|
||||
|
||||
const span = spans[0];
|
||||
expect(span?.attributes[SEMATTRS_E2B_FILE_FORMAT]).toBe("bytes");
|
||||
});
|
||||
|
||||
it("instruments sandbox.files.write() for single file", async () => {
|
||||
const sandbox = createMockSandbox();
|
||||
instrumentSandbox(sandbox);
|
||||
|
||||
await sandbox.files.write("/path/to/file.txt", "test content");
|
||||
|
||||
const spans = exporter.getFinishedSpans();
|
||||
expect(spans).toHaveLength(1);
|
||||
|
||||
const span = spans[0];
|
||||
expect(span?.name).toBe("e2b.files.write");
|
||||
expect(span?.attributes[SEMATTRS_E2B_FILE_OPERATION]).toBe("write");
|
||||
expect(span?.attributes[SEMATTRS_E2B_FILE_PATH]).toBe("/path/to/file.txt");
|
||||
expect(span?.attributes[SEMATTRS_E2B_FILE_SIZE_BYTES]).toBe(12); // "test content".length
|
||||
expect(span?.status.code).toBe(SpanStatusCode.OK);
|
||||
});
|
||||
|
||||
it("instruments sandbox.files.write() for multiple files", async () => {
|
||||
const sandbox = createMockSandbox();
|
||||
instrumentSandbox(sandbox);
|
||||
|
||||
const files = [
|
||||
{ path: "/file1.txt", data: "content1" },
|
||||
{ path: "/file2.txt", data: "content2" },
|
||||
{ path: "/file3.txt", data: "content3" },
|
||||
];
|
||||
|
||||
await (sandbox.files as any).write(files);
|
||||
|
||||
const spans = exporter.getFinishedSpans();
|
||||
expect(spans).toHaveLength(1);
|
||||
|
||||
const span = spans[0];
|
||||
expect(span?.name).toBe("e2b.files.write");
|
||||
expect(span?.attributes[SEMATTRS_E2B_FILE_COUNT]).toBe(3);
|
||||
expect(span?.status.code).toBe(SpanStatusCode.OK);
|
||||
});
|
||||
|
||||
it("instruments sandbox.files.list() and captures file count", async () => {
|
||||
const sandbox = createMockSandbox();
|
||||
instrumentSandbox(sandbox);
|
||||
|
||||
const files = await sandbox.files.list("/path/to/dir");
|
||||
|
||||
expect(files).toHaveLength(2);
|
||||
|
||||
const spans = exporter.getFinishedSpans();
|
||||
expect(spans).toHaveLength(1);
|
||||
|
||||
const span = spans[0];
|
||||
expect(span?.name).toBe("e2b.files.list");
|
||||
expect(span?.attributes[SEMATTRS_E2B_FILE_OPERATION]).toBe("list");
|
||||
expect(span?.attributes[SEMATTRS_E2B_FILE_PATH]).toBe("/path/to/dir");
|
||||
expect(span?.attributes[SEMATTRS_E2B_FILE_COUNT]).toBe(2);
|
||||
expect(span?.status.code).toBe(SpanStatusCode.OK);
|
||||
});
|
||||
|
||||
it("instruments sandbox.files.remove()", async () => {
|
||||
const sandbox = createMockSandbox();
|
||||
instrumentSandbox(sandbox);
|
||||
|
||||
await sandbox.files.remove("/path/to/file.txt");
|
||||
|
||||
const spans = exporter.getFinishedSpans();
|
||||
expect(spans).toHaveLength(1);
|
||||
|
||||
const span = spans[0];
|
||||
expect(span?.name).toBe("e2b.files.remove");
|
||||
expect(span?.attributes[SEMATTRS_E2B_FILE_OPERATION]).toBe("remove");
|
||||
expect(span?.attributes[SEMATTRS_E2B_FILE_PATH]).toBe("/path/to/file.txt");
|
||||
expect(span?.status.code).toBe(SpanStatusCode.OK);
|
||||
});
|
||||
|
||||
it("instruments sandbox.files.makeDir()", async () => {
|
||||
const sandbox = createMockSandbox();
|
||||
instrumentSandbox(sandbox);
|
||||
|
||||
await sandbox.files.makeDir("/path/to/new/dir");
|
||||
|
||||
const spans = exporter.getFinishedSpans();
|
||||
expect(spans).toHaveLength(1);
|
||||
|
||||
const span = spans[0];
|
||||
expect(span?.name).toBe("e2b.files.makeDir");
|
||||
expect(span?.attributes[SEMATTRS_E2B_FILE_OPERATION]).toBe("makeDir");
|
||||
expect(span?.attributes[SEMATTRS_E2B_FILE_PATH]).toBe("/path/to/new/dir");
|
||||
expect(span?.status.code).toBe(SpanStatusCode.OK);
|
||||
});
|
||||
|
||||
it("instruments sandbox.commands.run() and captures command results", async () => {
|
||||
const sandbox = createMockSandbox();
|
||||
instrumentSandbox(sandbox, { captureCommandOutput: true });
|
||||
|
||||
const result = await sandbox.commands.run("echo hello");
|
||||
|
||||
expect(result.exitCode).toBe(0);
|
||||
|
||||
const spans = exporter.getFinishedSpans();
|
||||
expect(spans).toHaveLength(1);
|
||||
|
||||
const span = spans[0];
|
||||
expect(span?.name).toBe("e2b.command.run");
|
||||
expect(span?.attributes[SEMATTRS_E2B_OPERATION]).toBe("command.run");
|
||||
expect(span?.attributes[SEMATTRS_E2B_COMMAND_EXIT_CODE]).toBe(0);
|
||||
expect(span?.attributes[SEMATTRS_E2B_COMMAND_STDOUT_LINES]).toBe(2);
|
||||
expect(span?.attributes[SEMATTRS_E2B_COMMAND_STDERR_LINES]).toBe(0);
|
||||
expect(span?.attributes[SEMATTRS_E2B_COMMAND_BACKGROUND]).toBe(false);
|
||||
expect(span?.status.code).toBe(SpanStatusCode.OK);
|
||||
});
|
||||
|
||||
it("instruments sandbox.commands.run() in background mode", async () => {
|
||||
const sandbox = createMockSandbox();
|
||||
instrumentSandbox(sandbox);
|
||||
|
||||
await sandbox.commands.run("long-running-command", { background: true });
|
||||
|
||||
const spans = exporter.getFinishedSpans();
|
||||
expect(spans).toHaveLength(1);
|
||||
|
||||
const span = spans[0];
|
||||
expect(span?.attributes[SEMATTRS_E2B_COMMAND_BACKGROUND]).toBe(true);
|
||||
expect(span?.status.code).toBe(SpanStatusCode.OK);
|
||||
});
|
||||
|
||||
it("captures errors and marks span status", async () => {
|
||||
const sandbox = createMockSandbox();
|
||||
sandbox.kill = vi.fn().mockRejectedValue(new Error("Failed to kill"));
|
||||
|
||||
instrumentSandbox(sandbox);
|
||||
|
||||
await expect(async () => sandbox.kill()).rejects.toThrowError(
|
||||
"Failed to kill"
|
||||
);
|
||||
|
||||
const spans = exporter.getFinishedSpans();
|
||||
expect(spans).toHaveLength(1);
|
||||
|
||||
const span = spans[0];
|
||||
expect(span?.status.code).toBe(SpanStatusCode.ERROR);
|
||||
const hasException = span?.events.some(
|
||||
(event) => event.name === "exception"
|
||||
);
|
||||
expect(hasException).toBe(true);
|
||||
});
|
||||
|
||||
it("is idempotent - calling instrument twice doesn't double-wrap", async () => {
|
||||
const sandbox = createMockSandbox();
|
||||
|
||||
const first = instrumentSandbox(sandbox);
|
||||
const second = instrumentSandbox(first);
|
||||
|
||||
expect(first).toBe(second);
|
||||
|
||||
await sandbox.kill();
|
||||
|
||||
// Should only create one span, not two
|
||||
const spans = exporter.getFinishedSpans();
|
||||
expect(spans).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("respects configuration options - captureFilePaths", async () => {
|
||||
const sandbox = createMockSandbox();
|
||||
instrumentSandbox(sandbox, { captureFilePaths: false });
|
||||
|
||||
await sandbox.files.read("/secret/path.txt");
|
||||
|
||||
const spans = exporter.getFinishedSpans();
|
||||
expect(spans).toHaveLength(1);
|
||||
|
||||
const span = spans[0];
|
||||
expect(span?.attributes[SEMATTRS_E2B_FILE_PATH]).toBeUndefined();
|
||||
});
|
||||
|
||||
it("respects configuration options - captureFileSize", async () => {
|
||||
const sandbox = createMockSandbox();
|
||||
instrumentSandbox(sandbox, { captureFileSize: false });
|
||||
|
||||
await sandbox.files.read("/path/file.txt");
|
||||
|
||||
const spans = exporter.getFinishedSpans();
|
||||
expect(spans).toHaveLength(1);
|
||||
|
||||
const span = spans[0];
|
||||
expect(span?.attributes[SEMATTRS_E2B_FILE_SIZE_BYTES]).toBeUndefined();
|
||||
});
|
||||
|
||||
it("respects configuration options - captureCodeLanguage", async () => {
|
||||
const sandbox = createMockSandbox();
|
||||
instrumentSandbox(sandbox, { captureCodeLanguage: false });
|
||||
|
||||
await (sandbox as any).runCode('print("test")', { language: "python" });
|
||||
|
||||
const spans = exporter.getFinishedSpans();
|
||||
expect(spans).toHaveLength(1);
|
||||
|
||||
const span = spans[0];
|
||||
expect(span?.attributes[SEMATTRS_E2B_CODE_LANGUAGE]).toBeUndefined();
|
||||
});
|
||||
|
||||
it("respects configuration options - captureCommandOutput disabled", async () => {
|
||||
const sandbox = createMockSandbox();
|
||||
instrumentSandbox(sandbox, { captureCommandOutput: false });
|
||||
|
||||
await sandbox.commands.run("echo test");
|
||||
|
||||
const spans = exporter.getFinishedSpans();
|
||||
expect(spans).toHaveLength(1);
|
||||
|
||||
const span = spans[0];
|
||||
expect(span?.attributes[SEMATTRS_E2B_COMMAND_STDOUT_LINES]).toBeUndefined();
|
||||
expect(span?.attributes[SEMATTRS_E2B_COMMAND_STDERR_LINES]).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("instrumentSandboxClass", () => {
|
||||
let provider: BasicTracerProvider;
|
||||
let exporter: InMemorySpanExporter;
|
||||
|
||||
beforeEach(() => {
|
||||
exporter = new InMemorySpanExporter();
|
||||
provider = new BasicTracerProvider({
|
||||
spanProcessors: [new SimpleSpanProcessor(exporter)],
|
||||
});
|
||||
trace.setGlobalTracerProvider(provider);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await provider.shutdown();
|
||||
exporter.reset();
|
||||
trace.disable();
|
||||
});
|
||||
|
||||
const createMockSandboxClass = () => {
|
||||
const mockSandbox = {
|
||||
sandboxId: "created-sandbox-456",
|
||||
kill: vi.fn(async () => {}),
|
||||
runCode: vi.fn(async () => ({
|
||||
results: [],
|
||||
logs: { stdout: [], stderr: [] },
|
||||
})),
|
||||
files: {
|
||||
read: vi.fn(async () => "content"),
|
||||
write: vi.fn(async () => ({ path: "/test.txt" })),
|
||||
list: vi.fn(async () => []),
|
||||
remove: vi.fn(async () => {}),
|
||||
makeDir: vi.fn(async () => true),
|
||||
},
|
||||
commands: {
|
||||
run: vi.fn(async () => ({ exitCode: 0, stdout: [], stderr: [] })),
|
||||
},
|
||||
} as unknown as Sandbox;
|
||||
|
||||
return {
|
||||
create: vi.fn(async () => mockSandbox),
|
||||
} as unknown as typeof Sandbox;
|
||||
};
|
||||
|
||||
it("instruments Sandbox.create() and records span", async () => {
|
||||
const SandboxClass = createMockSandboxClass();
|
||||
instrumentSandboxClass(SandboxClass);
|
||||
|
||||
const sandbox = await SandboxClass.create();
|
||||
|
||||
expect(sandbox.sandboxId).toBe("created-sandbox-456");
|
||||
|
||||
const spans = exporter.getFinishedSpans();
|
||||
expect(spans).toHaveLength(1);
|
||||
|
||||
const span = spans[0];
|
||||
expect(span?.name).toBe("e2b.sandbox.create");
|
||||
expect(span?.attributes[SEMATTRS_E2B_OPERATION]).toBe("sandbox.create");
|
||||
expect(span?.attributes[SEMATTRS_E2B_SANDBOX_ID]).toBe(
|
||||
"created-sandbox-456"
|
||||
);
|
||||
expect(span?.status.code).toBe(SpanStatusCode.OK);
|
||||
});
|
||||
|
||||
it("captures template from options", async () => {
|
||||
const SandboxClass = createMockSandboxClass();
|
||||
instrumentSandboxClass(SandboxClass);
|
||||
|
||||
await SandboxClass.create({ template: "custom-template" });
|
||||
|
||||
const spans = exporter.getFinishedSpans();
|
||||
expect(spans).toHaveLength(1);
|
||||
|
||||
const span = spans[0];
|
||||
expect(span?.attributes[SEMATTRS_E2B_SANDBOX_TEMPLATE]).toBe(
|
||||
"custom-template"
|
||||
);
|
||||
});
|
||||
|
||||
it("automatically instruments created sandbox instances", async () => {
|
||||
const SandboxClass = createMockSandboxClass();
|
||||
instrumentSandboxClass(SandboxClass);
|
||||
|
||||
const sandbox = await SandboxClass.create();
|
||||
|
||||
// Clear spans from create operation
|
||||
exporter.reset();
|
||||
|
||||
// Use the sandbox - should be automatically instrumented
|
||||
await sandbox.kill();
|
||||
|
||||
const spans = exporter.getFinishedSpans();
|
||||
expect(spans).toHaveLength(1);
|
||||
|
||||
const span = spans[0];
|
||||
expect(span?.name).toBe("e2b.sandbox.kill");
|
||||
expect(span?.status.code).toBe(SpanStatusCode.OK);
|
||||
});
|
||||
|
||||
it("is idempotent - calling instrumentSandboxClass twice doesn't double-wrap", async () => {
|
||||
const SandboxClass = createMockSandboxClass();
|
||||
|
||||
const first = instrumentSandboxClass(SandboxClass);
|
||||
const second = instrumentSandboxClass(first);
|
||||
|
||||
expect(first).toBe(second);
|
||||
|
||||
await SandboxClass.create();
|
||||
|
||||
// Should only create one span for create, not two
|
||||
const spans = exporter.getFinishedSpans();
|
||||
expect(spans).toHaveLength(1);
|
||||
expect(spans[0]?.name).toBe("e2b.sandbox.create");
|
||||
});
|
||||
|
||||
it("handles errors during sandbox creation", async () => {
|
||||
const SandboxClass = createMockSandboxClass();
|
||||
(SandboxClass as any).create = vi
|
||||
.fn()
|
||||
.mockRejectedValue(new Error("Failed to create sandbox"));
|
||||
|
||||
instrumentSandboxClass(SandboxClass);
|
||||
|
||||
await expect(async () => SandboxClass.create()).rejects.toThrowError(
|
||||
"Failed to create sandbox"
|
||||
);
|
||||
|
||||
const spans = exporter.getFinishedSpans();
|
||||
expect(spans).toHaveLength(1);
|
||||
|
||||
const span = spans[0];
|
||||
expect(span?.name).toBe("e2b.sandbox.create");
|
||||
expect(span?.status.code).toBe(SpanStatusCode.ERROR);
|
||||
const hasException = span?.events.some(
|
||||
(event) => event.name === "exception"
|
||||
);
|
||||
expect(hasException).toBe(true);
|
||||
});
|
||||
});
|
||||
672
packages/otel-e2b/src/index.ts
Normal file
672
packages/otel-e2b/src/index.ts
Normal file
@@ -0,0 +1,672 @@
|
||||
import {
|
||||
context,
|
||||
SpanKind,
|
||||
SpanStatusCode,
|
||||
trace,
|
||||
type Span,
|
||||
} from "@opentelemetry/api";
|
||||
import type { Sandbox } from "@e2b/code-interpreter";
|
||||
|
||||
const DEFAULT_TRACER_NAME = "@kubiks/otel-e2b";
|
||||
const INSTRUMENTED_FLAG = Symbol("kubiksOtelE2BInstrumented");
|
||||
const INSTRUMENTED_FILES_FLAG = Symbol("kubiksOtelE2BFilesInstrumented");
|
||||
const INSTRUMENTED_COMMANDS_FLAG = Symbol("kubiksOtelE2BCommandsInstrumented");
|
||||
|
||||
// Semantic attribute constants following OpenTelemetry conventions
|
||||
export const SEMATTRS_E2B_OPERATION = "e2b.operation" as const;
|
||||
export const SEMATTRS_E2B_SANDBOX_ID = "e2b.sandbox.id" as const;
|
||||
export const SEMATTRS_E2B_SANDBOX_TEMPLATE = "e2b.sandbox.template" as const;
|
||||
|
||||
// Code execution attributes
|
||||
export const SEMATTRS_E2B_CODE_LANGUAGE = "e2b.code.language" as const;
|
||||
export const SEMATTRS_E2B_CODE_HAS_ERROR = "e2b.code.has_error" as const;
|
||||
export const SEMATTRS_E2B_CODE_EXECUTION_COUNT =
|
||||
"e2b.code.execution_count" as const;
|
||||
|
||||
// Command execution attributes
|
||||
export const SEMATTRS_E2B_COMMAND_EXIT_CODE = "e2b.command.exit_code" as const;
|
||||
export const SEMATTRS_E2B_COMMAND_STDOUT_LINES =
|
||||
"e2b.command.stdout_lines" as const;
|
||||
export const SEMATTRS_E2B_COMMAND_STDERR_LINES =
|
||||
"e2b.command.stderr_lines" as const;
|
||||
export const SEMATTRS_E2B_COMMAND_BACKGROUND =
|
||||
"e2b.command.background" as const;
|
||||
|
||||
// File operation attributes
|
||||
export const SEMATTRS_E2B_FILE_OPERATION = "e2b.file.operation" as const;
|
||||
export const SEMATTRS_E2B_FILE_PATH = "e2b.file.path" as const;
|
||||
export const SEMATTRS_E2B_FILE_SIZE_BYTES = "e2b.file.size_bytes" as const;
|
||||
export const SEMATTRS_E2B_FILE_FORMAT = "e2b.file.format" as const;
|
||||
export const SEMATTRS_E2B_FILE_COUNT = "e2b.file.count" as const;
|
||||
|
||||
/**
|
||||
* Configuration options for E2B instrumentation.
|
||||
*/
|
||||
export interface InstrumentE2BConfig {
|
||||
/**
|
||||
* Custom tracer name. Defaults to "@kubiks/otel-e2b".
|
||||
*/
|
||||
tracerName?: string;
|
||||
|
||||
/**
|
||||
* Whether to capture file paths in spans.
|
||||
* Paths only, not content.
|
||||
* @default true
|
||||
*/
|
||||
captureFilePaths?: boolean;
|
||||
|
||||
/**
|
||||
* Whether to capture file sizes in spans.
|
||||
* @default true
|
||||
*/
|
||||
captureFileSize?: boolean;
|
||||
|
||||
/**
|
||||
* Whether to capture code language in spans.
|
||||
* @default true
|
||||
*/
|
||||
captureCodeLanguage?: boolean;
|
||||
|
||||
/**
|
||||
* Whether to capture command output line counts.
|
||||
* Only counts, not actual content.
|
||||
* @default false
|
||||
*/
|
||||
captureCommandOutput?: boolean;
|
||||
}
|
||||
|
||||
interface InstrumentedSandbox {
|
||||
[INSTRUMENTED_FLAG]?: true;
|
||||
}
|
||||
|
||||
interface InstrumentedFilesystem {
|
||||
[INSTRUMENTED_FILES_FLAG]?: true;
|
||||
}
|
||||
|
||||
interface InstrumentedCommands {
|
||||
[INSTRUMENTED_COMMANDS_FLAG]?: true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finalizes a span with status, timing, and optional error.
|
||||
*/
|
||||
function finalizeSpan(span: Span, error?: unknown): void {
|
||||
if (error) {
|
||||
if (error instanceof Error) {
|
||||
span.recordException(error);
|
||||
} else {
|
||||
span.recordException(new Error(String(error)));
|
||||
}
|
||||
span.setStatus({ code: SpanStatusCode.ERROR });
|
||||
} else {
|
||||
span.setStatus({ code: SpanStatusCode.OK });
|
||||
}
|
||||
span.end();
|
||||
}
|
||||
|
||||
/**
|
||||
* Instruments the filesystem module of a sandbox.
|
||||
*/
|
||||
function instrumentFilesystem(
|
||||
files: any,
|
||||
sandboxId: string,
|
||||
tracer: ReturnType<typeof trace.getTracer>,
|
||||
config?: InstrumentE2BConfig
|
||||
): any {
|
||||
if (!files) {
|
||||
return files;
|
||||
}
|
||||
|
||||
// Check if already instrumented
|
||||
if ((files as InstrumentedFilesystem)[INSTRUMENTED_FILES_FLAG]) {
|
||||
return files;
|
||||
}
|
||||
|
||||
const { captureFilePaths = true, captureFileSize = true } = config ?? {};
|
||||
|
||||
// Instrument read
|
||||
const originalRead = files.read?.bind(files);
|
||||
if (originalRead) {
|
||||
files.read = async function instrumentedRead(
|
||||
path: string,
|
||||
opts?: any
|
||||
): Promise<any> {
|
||||
const span = tracer.startSpan("e2b.files.read", {
|
||||
kind: SpanKind.CLIENT,
|
||||
});
|
||||
|
||||
span.setAttributes({
|
||||
[SEMATTRS_E2B_OPERATION]: "files.read",
|
||||
[SEMATTRS_E2B_SANDBOX_ID]: sandboxId,
|
||||
[SEMATTRS_E2B_FILE_OPERATION]: "read",
|
||||
});
|
||||
|
||||
if (captureFilePaths && path) {
|
||||
span.setAttribute(SEMATTRS_E2B_FILE_PATH, path);
|
||||
}
|
||||
|
||||
if (opts?.format) {
|
||||
span.setAttribute(SEMATTRS_E2B_FILE_FORMAT, opts.format);
|
||||
}
|
||||
|
||||
const activeContext = trace.setSpan(context.active(), span);
|
||||
|
||||
try {
|
||||
const result = await context.with(activeContext, () =>
|
||||
originalRead(path, opts)
|
||||
);
|
||||
|
||||
// Capture size if result is a string or has a length/size property
|
||||
if (captureFileSize) {
|
||||
if (typeof result === "string") {
|
||||
span.setAttribute(SEMATTRS_E2B_FILE_SIZE_BYTES, result.length);
|
||||
} else if (result?.length !== undefined) {
|
||||
span.setAttribute(SEMATTRS_E2B_FILE_SIZE_BYTES, result.length);
|
||||
} else if (result?.size !== undefined) {
|
||||
span.setAttribute(SEMATTRS_E2B_FILE_SIZE_BYTES, result.size);
|
||||
}
|
||||
}
|
||||
|
||||
finalizeSpan(span);
|
||||
return result;
|
||||
} catch (error) {
|
||||
finalizeSpan(span, error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Instrument write (handles both single and multiple file writes)
|
||||
const originalWrite = files.write?.bind(files);
|
||||
if (originalWrite) {
|
||||
files.write = async function instrumentedWrite(
|
||||
pathOrFiles: string | any[],
|
||||
dataOrOpts?: any,
|
||||
opts?: any
|
||||
): Promise<any> {
|
||||
const span = tracer.startSpan("e2b.files.write", {
|
||||
kind: SpanKind.CLIENT,
|
||||
});
|
||||
|
||||
span.setAttributes({
|
||||
[SEMATTRS_E2B_OPERATION]: "files.write",
|
||||
[SEMATTRS_E2B_SANDBOX_ID]: sandboxId,
|
||||
[SEMATTRS_E2B_FILE_OPERATION]: "write",
|
||||
});
|
||||
|
||||
const isArray = Array.isArray(pathOrFiles);
|
||||
|
||||
if (isArray) {
|
||||
// Multiple files
|
||||
span.setAttribute(SEMATTRS_E2B_FILE_COUNT, pathOrFiles.length);
|
||||
} else if (captureFilePaths && typeof pathOrFiles === "string") {
|
||||
// Single file
|
||||
span.setAttribute(SEMATTRS_E2B_FILE_PATH, pathOrFiles);
|
||||
|
||||
// Try to capture size
|
||||
if (captureFileSize && dataOrOpts) {
|
||||
if (typeof dataOrOpts === "string") {
|
||||
span.setAttribute(SEMATTRS_E2B_FILE_SIZE_BYTES, dataOrOpts.length);
|
||||
} else if (dataOrOpts?.length !== undefined) {
|
||||
span.setAttribute(SEMATTRS_E2B_FILE_SIZE_BYTES, dataOrOpts.length);
|
||||
} else if (dataOrOpts?.size !== undefined) {
|
||||
span.setAttribute(SEMATTRS_E2B_FILE_SIZE_BYTES, dataOrOpts.size);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const activeContext = trace.setSpan(context.active(), span);
|
||||
|
||||
try {
|
||||
const result = await context.with(activeContext, () =>
|
||||
originalWrite(pathOrFiles, dataOrOpts, opts)
|
||||
);
|
||||
|
||||
finalizeSpan(span);
|
||||
return result;
|
||||
} catch (error) {
|
||||
finalizeSpan(span, error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Instrument list
|
||||
const originalList = files.list?.bind(files);
|
||||
if (originalList) {
|
||||
files.list = async function instrumentedList(
|
||||
path: string,
|
||||
opts?: any
|
||||
): Promise<any> {
|
||||
const span = tracer.startSpan("e2b.files.list", {
|
||||
kind: SpanKind.CLIENT,
|
||||
});
|
||||
|
||||
span.setAttributes({
|
||||
[SEMATTRS_E2B_OPERATION]: "files.list",
|
||||
[SEMATTRS_E2B_SANDBOX_ID]: sandboxId,
|
||||
[SEMATTRS_E2B_FILE_OPERATION]: "list",
|
||||
});
|
||||
|
||||
if (captureFilePaths && path) {
|
||||
span.setAttribute(SEMATTRS_E2B_FILE_PATH, path);
|
||||
}
|
||||
|
||||
const activeContext = trace.setSpan(context.active(), span);
|
||||
|
||||
try {
|
||||
const result = await context.with(activeContext, () =>
|
||||
originalList(path, opts)
|
||||
);
|
||||
|
||||
if (Array.isArray(result)) {
|
||||
span.setAttribute(SEMATTRS_E2B_FILE_COUNT, result.length);
|
||||
}
|
||||
|
||||
finalizeSpan(span);
|
||||
return result;
|
||||
} catch (error) {
|
||||
finalizeSpan(span, error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Instrument remove
|
||||
const originalRemove = files.remove?.bind(files);
|
||||
if (originalRemove) {
|
||||
files.remove = async function instrumentedRemove(
|
||||
path: string,
|
||||
opts?: any
|
||||
): Promise<any> {
|
||||
const span = tracer.startSpan("e2b.files.remove", {
|
||||
kind: SpanKind.CLIENT,
|
||||
});
|
||||
|
||||
span.setAttributes({
|
||||
[SEMATTRS_E2B_OPERATION]: "files.remove",
|
||||
[SEMATTRS_E2B_SANDBOX_ID]: sandboxId,
|
||||
[SEMATTRS_E2B_FILE_OPERATION]: "remove",
|
||||
});
|
||||
|
||||
if (captureFilePaths && path) {
|
||||
span.setAttribute(SEMATTRS_E2B_FILE_PATH, path);
|
||||
}
|
||||
|
||||
const activeContext = trace.setSpan(context.active(), span);
|
||||
|
||||
try {
|
||||
const result = await context.with(activeContext, () =>
|
||||
originalRemove(path, opts)
|
||||
);
|
||||
|
||||
finalizeSpan(span);
|
||||
return result;
|
||||
} catch (error) {
|
||||
finalizeSpan(span, error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Instrument makeDir
|
||||
const originalMakeDir = files.makeDir?.bind(files);
|
||||
if (originalMakeDir) {
|
||||
files.makeDir = async function instrumentedMakeDir(
|
||||
path: string,
|
||||
opts?: any
|
||||
): Promise<any> {
|
||||
const span = tracer.startSpan("e2b.files.makeDir", {
|
||||
kind: SpanKind.CLIENT,
|
||||
});
|
||||
|
||||
span.setAttributes({
|
||||
[SEMATTRS_E2B_OPERATION]: "files.makeDir",
|
||||
[SEMATTRS_E2B_SANDBOX_ID]: sandboxId,
|
||||
[SEMATTRS_E2B_FILE_OPERATION]: "makeDir",
|
||||
});
|
||||
|
||||
if (captureFilePaths && path) {
|
||||
span.setAttribute(SEMATTRS_E2B_FILE_PATH, path);
|
||||
}
|
||||
|
||||
const activeContext = trace.setSpan(context.active(), span);
|
||||
|
||||
try {
|
||||
const result = await context.with(activeContext, () =>
|
||||
originalMakeDir(path, opts)
|
||||
);
|
||||
|
||||
finalizeSpan(span);
|
||||
return result;
|
||||
} catch (error) {
|
||||
finalizeSpan(span, error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Mark as instrumented
|
||||
(files as InstrumentedFilesystem)[INSTRUMENTED_FILES_FLAG] = true;
|
||||
|
||||
return files;
|
||||
}
|
||||
|
||||
/**
|
||||
* Instruments the commands module of a sandbox.
|
||||
*/
|
||||
function instrumentCommands(
|
||||
commands: any,
|
||||
sandboxId: string,
|
||||
tracer: ReturnType<typeof trace.getTracer>,
|
||||
config?: InstrumentE2BConfig
|
||||
): any {
|
||||
if (!commands) {
|
||||
return commands;
|
||||
}
|
||||
|
||||
// Check if already instrumented
|
||||
if ((commands as InstrumentedCommands)[INSTRUMENTED_COMMANDS_FLAG]) {
|
||||
return commands;
|
||||
}
|
||||
|
||||
const { captureCommandOutput = false } = config ?? {};
|
||||
|
||||
// Instrument run
|
||||
const originalRun = commands.run?.bind(commands);
|
||||
if (originalRun) {
|
||||
commands.run = async function instrumentedRun(
|
||||
cmd: string,
|
||||
opts?: any
|
||||
): Promise<any> {
|
||||
const span = tracer.startSpan("e2b.command.run", {
|
||||
kind: SpanKind.CLIENT,
|
||||
});
|
||||
|
||||
const isBackground = opts?.background === true;
|
||||
|
||||
span.setAttributes({
|
||||
[SEMATTRS_E2B_OPERATION]: "command.run",
|
||||
[SEMATTRS_E2B_SANDBOX_ID]: sandboxId,
|
||||
[SEMATTRS_E2B_COMMAND_BACKGROUND]: isBackground,
|
||||
});
|
||||
|
||||
const activeContext = trace.setSpan(context.active(), span);
|
||||
|
||||
try {
|
||||
const result = await context.with(activeContext, () =>
|
||||
originalRun(cmd, opts)
|
||||
);
|
||||
|
||||
// For non-background commands, capture result details
|
||||
if (!isBackground && result) {
|
||||
if (result.exitCode !== undefined) {
|
||||
span.setAttribute(SEMATTRS_E2B_COMMAND_EXIT_CODE, result.exitCode);
|
||||
}
|
||||
|
||||
if (captureCommandOutput) {
|
||||
if (result.stdout !== undefined) {
|
||||
const stdoutLines = Array.isArray(result.stdout)
|
||||
? result.stdout.length
|
||||
: typeof result.stdout === "string"
|
||||
? result.stdout.split("\n").length
|
||||
: 0;
|
||||
span.setAttribute(SEMATTRS_E2B_COMMAND_STDOUT_LINES, stdoutLines);
|
||||
}
|
||||
|
||||
if (result.stderr !== undefined) {
|
||||
const stderrLines = Array.isArray(result.stderr)
|
||||
? result.stderr.length
|
||||
: typeof result.stderr === "string"
|
||||
? result.stderr.split("\n").length
|
||||
: 0;
|
||||
span.setAttribute(SEMATTRS_E2B_COMMAND_STDERR_LINES, stderrLines);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
finalizeSpan(span);
|
||||
return result;
|
||||
} catch (error) {
|
||||
finalizeSpan(span, error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Mark as instrumented
|
||||
(commands as InstrumentedCommands)[INSTRUMENTED_COMMANDS_FLAG] = true;
|
||||
|
||||
return commands;
|
||||
}
|
||||
|
||||
/**
|
||||
* Instruments an E2B Sandbox instance with OpenTelemetry tracing.
|
||||
*
|
||||
* This function wraps sandbox methods to create spans for each operation.
|
||||
* The instrumentation is idempotent - calling it multiple times on the same
|
||||
* sandbox will only instrument it once.
|
||||
*
|
||||
* @param sandbox - The E2B Sandbox to instrument
|
||||
* @param config - Optional configuration for instrumentation behavior
|
||||
* @returns The instrumented sandbox (same instance, modified in place)
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { Sandbox } from '@e2b/code-interpreter';
|
||||
* import { instrumentSandbox } from '@kubiks/otel-e2b';
|
||||
*
|
||||
* const sandbox = await Sandbox.create();
|
||||
* instrumentSandbox(sandbox, {
|
||||
* captureFilePaths: true,
|
||||
* captureFileSize: true,
|
||||
* });
|
||||
*
|
||||
* // All operations are now traced
|
||||
* await sandbox.runCode('print("Hello")');
|
||||
* ```
|
||||
*/
|
||||
export function instrumentSandbox<T extends Sandbox>(
|
||||
sandbox: T,
|
||||
config?: InstrumentE2BConfig
|
||||
): T {
|
||||
if (!sandbox) {
|
||||
return sandbox;
|
||||
}
|
||||
|
||||
// Check if already instrumented
|
||||
if ((sandbox as any)[INSTRUMENTED_FLAG]) {
|
||||
return sandbox;
|
||||
}
|
||||
|
||||
const { tracerName = DEFAULT_TRACER_NAME, captureCodeLanguage = true } =
|
||||
config ?? {};
|
||||
|
||||
const tracer = trace.getTracer(tracerName);
|
||||
const sandboxId = sandbox.sandboxId || "unknown";
|
||||
|
||||
// Instrument kill method
|
||||
const originalKill = sandbox.kill?.bind(sandbox);
|
||||
if (originalKill) {
|
||||
(sandbox as any).kill = async function instrumentedKill(
|
||||
opts?: any
|
||||
): Promise<void> {
|
||||
const span = tracer.startSpan("e2b.sandbox.kill", {
|
||||
kind: SpanKind.CLIENT,
|
||||
});
|
||||
|
||||
span.setAttributes({
|
||||
[SEMATTRS_E2B_OPERATION]: "sandbox.kill",
|
||||
[SEMATTRS_E2B_SANDBOX_ID]: sandboxId,
|
||||
});
|
||||
|
||||
const activeContext = trace.setSpan(context.active(), span);
|
||||
|
||||
try {
|
||||
await context.with(activeContext, () => originalKill(opts));
|
||||
finalizeSpan(span);
|
||||
} catch (error) {
|
||||
finalizeSpan(span, error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Instrument runCode method (code-interpreter specific)
|
||||
const originalRunCode = (sandbox as any).runCode?.bind(sandbox);
|
||||
if (originalRunCode) {
|
||||
(sandbox as any).runCode = async function instrumentedRunCode(
|
||||
code: string,
|
||||
opts?: any
|
||||
): Promise<any> {
|
||||
const span = tracer.startSpan("e2b.code.run", {
|
||||
kind: SpanKind.CLIENT,
|
||||
});
|
||||
|
||||
span.setAttributes({
|
||||
[SEMATTRS_E2B_OPERATION]: "code.run",
|
||||
[SEMATTRS_E2B_SANDBOX_ID]: sandboxId,
|
||||
});
|
||||
|
||||
// Capture language if available
|
||||
if (captureCodeLanguage) {
|
||||
const language = opts?.language || opts?.context?.language || "python";
|
||||
span.setAttribute(SEMATTRS_E2B_CODE_LANGUAGE, language);
|
||||
}
|
||||
|
||||
const activeContext = trace.setSpan(context.active(), span);
|
||||
|
||||
try {
|
||||
const result = await context.with(activeContext, () =>
|
||||
originalRunCode(code, opts)
|
||||
);
|
||||
|
||||
// Capture execution details
|
||||
if (result) {
|
||||
// Always set has_error attribute (true if error exists, false otherwise)
|
||||
span.setAttribute(SEMATTRS_E2B_CODE_HAS_ERROR, !!result.error);
|
||||
|
||||
if (result.executionCount !== undefined) {
|
||||
span.setAttribute(
|
||||
SEMATTRS_E2B_CODE_EXECUTION_COUNT,
|
||||
result.executionCount
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
finalizeSpan(span);
|
||||
return result;
|
||||
} catch (error) {
|
||||
finalizeSpan(span, error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Instrument filesystem module
|
||||
if (sandbox.files) {
|
||||
instrumentFilesystem(sandbox.files, sandboxId, tracer, config);
|
||||
}
|
||||
|
||||
// Instrument commands module
|
||||
if (sandbox.commands) {
|
||||
instrumentCommands(sandbox.commands, sandboxId, tracer, config);
|
||||
}
|
||||
|
||||
// Mark as instrumented
|
||||
(sandbox as any)[INSTRUMENTED_FLAG] = true;
|
||||
|
||||
return sandbox;
|
||||
}
|
||||
|
||||
/**
|
||||
* Instruments the Sandbox class itself to automatically instrument all created sandboxes.
|
||||
*
|
||||
* This function wraps the static `Sandbox.create()` method to automatically
|
||||
* instrument any sandbox instances it creates.
|
||||
*
|
||||
* @param SandboxClass - The Sandbox class to instrument
|
||||
* @param config - Optional configuration for instrumentation behavior
|
||||
* @returns The instrumented Sandbox class (same class, modified in place)
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { Sandbox } from '@e2b/code-interpreter';
|
||||
* import { instrumentSandboxClass } from '@kubiks/otel-e2b';
|
||||
*
|
||||
* // Instrument the class once at startup
|
||||
* instrumentSandboxClass(Sandbox, {
|
||||
* captureFilePaths: true,
|
||||
* captureFileSize: true,
|
||||
* });
|
||||
*
|
||||
* // All sandboxes created after this are automatically instrumented
|
||||
* const sandbox = await Sandbox.create();
|
||||
* await sandbox.runCode('print("Hello")'); // Automatically traced
|
||||
* ```
|
||||
*/
|
||||
export function instrumentSandboxClass<T extends typeof Sandbox>(
|
||||
SandboxClass: T,
|
||||
config?: InstrumentE2BConfig
|
||||
): T {
|
||||
if (!SandboxClass) {
|
||||
return SandboxClass;
|
||||
}
|
||||
|
||||
// Check if already instrumented
|
||||
if ((SandboxClass as any)[INSTRUMENTED_FLAG]) {
|
||||
return SandboxClass;
|
||||
}
|
||||
|
||||
const { tracerName = DEFAULT_TRACER_NAME } = config ?? {};
|
||||
const tracer = trace.getTracer(tracerName);
|
||||
|
||||
// Instrument the static create method
|
||||
const originalCreate = SandboxClass.create?.bind(SandboxClass);
|
||||
if (originalCreate) {
|
||||
(SandboxClass as any).create = async function instrumentedCreate(
|
||||
opts?: any
|
||||
): Promise<Sandbox> {
|
||||
const span = tracer.startSpan("e2b.sandbox.create", {
|
||||
kind: SpanKind.CLIENT,
|
||||
});
|
||||
|
||||
span.setAttributes({
|
||||
[SEMATTRS_E2B_OPERATION]: "sandbox.create",
|
||||
});
|
||||
|
||||
// Capture template if available
|
||||
if (opts?.template) {
|
||||
span.setAttribute(SEMATTRS_E2B_SANDBOX_TEMPLATE, opts.template);
|
||||
}
|
||||
|
||||
const activeContext = trace.setSpan(context.active(), span);
|
||||
|
||||
try {
|
||||
const sandbox = await context.with(activeContext, () =>
|
||||
originalCreate(opts)
|
||||
);
|
||||
|
||||
// Add sandbox ID to span
|
||||
if (sandbox?.sandboxId) {
|
||||
span.setAttribute(SEMATTRS_E2B_SANDBOX_ID, sandbox.sandboxId);
|
||||
}
|
||||
|
||||
finalizeSpan(span);
|
||||
|
||||
// Automatically instrument the created sandbox
|
||||
instrumentSandbox(sandbox as Sandbox, config);
|
||||
|
||||
return sandbox as Sandbox;
|
||||
} catch (error) {
|
||||
finalizeSpan(span, error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Mark as instrumented
|
||||
(SandboxClass as any)[INSTRUMENTED_FLAG] = true;
|
||||
|
||||
return SandboxClass;
|
||||
}
|
||||
22
packages/otel-e2b/tsconfig.json
Normal file
22
packages/otel-e2b/tsconfig.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"lib": ["ES2020"],
|
||||
"outDir": "dist",
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"sourceMap": true,
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"declarationDir": "dist/types",
|
||||
"stripInternal": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist", "**/*.test.ts"]
|
||||
}
|
||||
|
||||
222
pnpm-lock.yaml
generated
222
pnpm-lock.yaml
generated
@@ -102,6 +102,30 @@ importers:
|
||||
specifier: 0.33.0
|
||||
version: 0.33.0(less@4.2.0)(sass@1.69.7)(stylus@0.59.0)
|
||||
|
||||
packages/otel-e2b:
|
||||
devDependencies:
|
||||
'@e2b/code-interpreter':
|
||||
specifier: ^2.0.0
|
||||
version: 2.2.0
|
||||
'@opentelemetry/api':
|
||||
specifier: ^1.9.0
|
||||
version: 1.9.0
|
||||
'@opentelemetry/sdk-trace-base':
|
||||
specifier: ^2.1.0
|
||||
version: 2.1.0(@opentelemetry/api@1.9.0)
|
||||
'@types/node':
|
||||
specifier: 18.15.11
|
||||
version: 18.15.11
|
||||
rimraf:
|
||||
specifier: 3.0.2
|
||||
version: 3.0.2
|
||||
typescript:
|
||||
specifier: ^5
|
||||
version: 5.3.3
|
||||
vitest:
|
||||
specifier: 0.33.0
|
||||
version: 0.33.0(less@4.2.0)(sass@1.69.7)(stylus@0.59.0)
|
||||
|
||||
packages/otel-inbound:
|
||||
devDependencies:
|
||||
'@inboundemail/sdk':
|
||||
@@ -284,6 +308,9 @@ packages:
|
||||
'@better-fetch/fetch@1.1.18':
|
||||
resolution: {integrity: sha512-rEFOE1MYIsBmoMJtQbl32PGHHXuG2hDxvEd7rUHE0vCBoFQVSDqaVs9hkZEtHCxRoY+CljXKFCOuJ8uxqw1LcA==}
|
||||
|
||||
'@bufbuild/protobuf@2.10.0':
|
||||
resolution: {integrity: sha512-fdRs9PSrBF7QUntpZpq6BTw58fhgGJojgg39m9oFOJGZT+nip9b0so5cYY1oWl5pvemDLr0cPPsH46vwThEbpQ==}
|
||||
|
||||
'@changesets/apply-release-plan@7.0.0':
|
||||
resolution: {integrity: sha512-vfi69JR416qC9hWmFGSxj7N6wA5J222XNBmezSVATPWDVPIF7gkd4d8CpbEbXmRWbVrkoli3oerGS6dcL/BGsQ==}
|
||||
|
||||
@@ -345,6 +372,21 @@ packages:
|
||||
'@changesets/write@0.3.0':
|
||||
resolution: {integrity: sha512-slGLb21fxZVUYbyea+94uFiD6ntQW0M2hIKNznFizDhZPDgn2c/fv1UzzlW43RVzh1BEDuIqW6hzlJ1OflNmcw==}
|
||||
|
||||
'@connectrpc/connect-web@2.0.0-rc.3':
|
||||
resolution: {integrity: sha512-w88P8Lsn5CCsA7MFRl2e6oLY4J/5toiNtJns/YJrlyQaWOy3RO8pDgkz+iIkG98RPMhj2thuBvsd3Cn4DKKCkw==}
|
||||
peerDependencies:
|
||||
'@bufbuild/protobuf': ^2.2.0
|
||||
'@connectrpc/connect': 2.0.0-rc.3
|
||||
|
||||
'@connectrpc/connect@2.0.0-rc.3':
|
||||
resolution: {integrity: sha512-ARBt64yEyKbanyRETTjcjJuHr2YXorzQo0etyS5+P6oSeW8xEuzajA9g+zDnMcj1hlX2dQE93foIWQGfpru7gQ==}
|
||||
peerDependencies:
|
||||
'@bufbuild/protobuf': ^2.2.0
|
||||
|
||||
'@e2b/code-interpreter@2.2.0':
|
||||
resolution: {integrity: sha512-j2XIhuuV+MvjCMJbwRCL3hZjkOTv4Jk1FygI4dCwaU9PggtAW+TXhyaVzzVPZroE9fqxLfrXGqY08P06Ei7MQA==}
|
||||
engines: {node: '>=20'}
|
||||
|
||||
'@esbuild/aix-ppc64@0.25.4':
|
||||
resolution: {integrity: sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -637,10 +679,22 @@ packages:
|
||||
react: '>=16.8.0'
|
||||
react-dom: '>=16.8.0'
|
||||
|
||||
'@isaacs/balanced-match@4.0.1':
|
||||
resolution: {integrity: sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==}
|
||||
engines: {node: 20 || >=22}
|
||||
|
||||
'@isaacs/brace-expansion@5.0.0':
|
||||
resolution: {integrity: sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==}
|
||||
engines: {node: 20 || >=22}
|
||||
|
||||
'@isaacs/cliui@8.0.2':
|
||||
resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
'@isaacs/fs-minipass@4.0.1':
|
||||
resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
|
||||
'@jest/schemas@29.6.3':
|
||||
resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==}
|
||||
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
|
||||
@@ -1082,6 +1136,10 @@ packages:
|
||||
resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==}
|
||||
engines: {node: '>= 8.10.0'}
|
||||
|
||||
chownr@3.0.0:
|
||||
resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
ci-info@3.9.0:
|
||||
resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -1114,6 +1172,9 @@ packages:
|
||||
resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==}
|
||||
engines: {node: '>=14'}
|
||||
|
||||
compare-versions@6.1.1:
|
||||
resolution: {integrity: sha512-4hm4VPpIecmlg59CHXnRDnqGplJFrbLG4aFEl5vl6cK1u76ws3LLvX7ikFnTDl5vo39sjWD6AaDPYodJp/NNHg==}
|
||||
|
||||
concat-map@0.0.1:
|
||||
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
|
||||
|
||||
@@ -1230,6 +1291,9 @@ packages:
|
||||
resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
dockerfile-ast@0.7.1:
|
||||
resolution: {integrity: sha512-oX/A4I0EhSkGqrFv0YuvPkBUSYp1XiY8O8zAKc8Djglx8ocz+JfOr8gP0ryRMC2myqvDLagmnZaU9ot1vG2ijw==}
|
||||
|
||||
dom-serializer@2.0.0:
|
||||
resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==}
|
||||
|
||||
@@ -1339,6 +1403,10 @@ packages:
|
||||
sqlite3:
|
||||
optional: true
|
||||
|
||||
e2b@2.6.2:
|
||||
resolution: {integrity: sha512-BQ2yzrBu4v48geRiTdsrHdqcWAV2zvlUq81Ont/KI7foYxk24ghdOwvOSURKJ+i1Y5vO8lDTIfsc0tWswAfOiQ==}
|
||||
engines: {node: '>=20'}
|
||||
|
||||
eastasianwidth@0.2.0:
|
||||
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
|
||||
|
||||
@@ -1497,6 +1565,11 @@ packages:
|
||||
resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==}
|
||||
hasBin: true
|
||||
|
||||
glob@11.0.3:
|
||||
resolution: {integrity: sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA==}
|
||||
engines: {node: 20 || >=22}
|
||||
hasBin: true
|
||||
|
||||
glob@7.2.3:
|
||||
resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==}
|
||||
deprecated: Glob versions prior to v9 are no longer supported
|
||||
@@ -1701,6 +1774,10 @@ packages:
|
||||
jackspeak@3.4.3:
|
||||
resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==}
|
||||
|
||||
jackspeak@4.1.1:
|
||||
resolution: {integrity: sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==}
|
||||
engines: {node: 20 || >=22}
|
||||
|
||||
jose@5.10.0:
|
||||
resolution: {integrity: sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg==}
|
||||
|
||||
@@ -1792,6 +1869,10 @@ packages:
|
||||
lru-cache@10.4.3:
|
||||
resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==}
|
||||
|
||||
lru-cache@11.2.2:
|
||||
resolution: {integrity: sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg==}
|
||||
engines: {node: 20 || >=22}
|
||||
|
||||
lru-cache@4.1.5:
|
||||
resolution: {integrity: sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==}
|
||||
|
||||
@@ -1839,6 +1920,10 @@ packages:
|
||||
resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==}
|
||||
engines: {node: '>=4'}
|
||||
|
||||
minimatch@10.1.1:
|
||||
resolution: {integrity: sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==}
|
||||
engines: {node: 20 || >=22}
|
||||
|
||||
minimatch@3.1.2:
|
||||
resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
|
||||
|
||||
@@ -1858,6 +1943,10 @@ packages:
|
||||
resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==}
|
||||
engines: {node: '>=16 || 14 >=14.17'}
|
||||
|
||||
minizlib@3.1.0:
|
||||
resolution: {integrity: sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==}
|
||||
engines: {node: '>= 18'}
|
||||
|
||||
mixme@0.5.10:
|
||||
resolution: {integrity: sha512-5H76ANWinB1H3twpJ6JY8uvAtpmFvHNArpilJAjXRKXSDDLPIMoZArw5SH0q9z+lLs8IrMw7Q2VWpWimFKFT1Q==}
|
||||
engines: {node: '>= 8.0.0'}
|
||||
@@ -1903,11 +1992,6 @@ packages:
|
||||
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
||||
hasBin: true
|
||||
|
||||
nanoid@3.3.7:
|
||||
resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==}
|
||||
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
||||
hasBin: true
|
||||
|
||||
nanostores@1.0.1:
|
||||
resolution: {integrity: sha512-kNZ9xnoJYKg/AfxjrVL4SS0fKX++4awQReGqWnwTRHxeHGZ1FJFVgTqr/eMrNQdp0Tz7M7tG/TDaX8QfHDwVCw==}
|
||||
engines: {node: ^20.0.0 || >=22.0.0}
|
||||
@@ -1956,6 +2040,12 @@ packages:
|
||||
once@1.4.0:
|
||||
resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
|
||||
|
||||
openapi-fetch@0.14.1:
|
||||
resolution: {integrity: sha512-l7RarRHxlEZYjMLd/PR0slfMVse2/vvIAGm75/F7J6MlQ8/b9uUQmUF2kCPrQhJqMXSxmYWObVgeYXbFYzZR+A==}
|
||||
|
||||
openapi-typescript-helpers@0.0.15:
|
||||
resolution: {integrity: sha512-opyTPaunsklCBpTK8JGef6mfPhLSnyy5a0IN9vKtx3+4aExf+KxEqYwIy3hqkedXIB97u357uLMJsOnm3GVjsw==}
|
||||
|
||||
os-tmpdir@1.0.2:
|
||||
resolution: {integrity: sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@@ -2028,6 +2118,10 @@ packages:
|
||||
resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==}
|
||||
engines: {node: '>=16 || 14 >=14.18'}
|
||||
|
||||
path-scurry@2.0.0:
|
||||
resolution: {integrity: sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==}
|
||||
engines: {node: 20 || >=22}
|
||||
|
||||
path-type@4.0.0:
|
||||
resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -2070,6 +2164,9 @@ packages:
|
||||
pkg-types@1.0.3:
|
||||
resolution: {integrity: sha512-nN7pYi0AQqJnoLPC9eHFQ8AcyaixBUOwvqc5TDnIKCMEE6I0y8P7OKA7fPexsXGCGxQDl/cmrLAp26LhcwxZ4A==}
|
||||
|
||||
platform@1.3.6:
|
||||
resolution: {integrity: sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg==}
|
||||
|
||||
postcss@8.4.33:
|
||||
resolution: {integrity: sha512-Kkpbhhdjw2qQs2O2DGX+8m5OVqEcbB9HRBvuYM9pgrjEFUg30A9LmXNlTAUj4S9kgtGyrMbTzVjH7E+s5Re2yg==}
|
||||
engines: {node: ^10 || ^12 || >=14}
|
||||
@@ -2425,6 +2522,10 @@ packages:
|
||||
peerDependencies:
|
||||
react: ^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
|
||||
tar@7.5.2:
|
||||
resolution: {integrity: sha512-7NyxrTE4Anh8km8iEy7o0QYPs+0JKBTj5ZaqHg6B39erLg0qYXN3BijtShwbsNSvQ+LN75+KV+C4QR/f6Gwnpg==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
term-size@2.2.1:
|
||||
resolution: {integrity: sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -2633,6 +2734,12 @@ packages:
|
||||
webdriverio:
|
||||
optional: true
|
||||
|
||||
vscode-languageserver-textdocument@1.0.12:
|
||||
resolution: {integrity: sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA==}
|
||||
|
||||
vscode-languageserver-types@3.17.5:
|
||||
resolution: {integrity: sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==}
|
||||
|
||||
wcwidth@1.0.1:
|
||||
resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==}
|
||||
|
||||
@@ -2710,6 +2817,10 @@ packages:
|
||||
yallist@4.0.0:
|
||||
resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==}
|
||||
|
||||
yallist@5.0.0:
|
||||
resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
yargs-parser@18.1.3:
|
||||
resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==}
|
||||
engines: {node: '>=6'}
|
||||
@@ -2810,6 +2921,8 @@ snapshots:
|
||||
|
||||
'@better-fetch/fetch@1.1.18': {}
|
||||
|
||||
'@bufbuild/protobuf@2.10.0': {}
|
||||
|
||||
'@changesets/apply-release-plan@7.0.0':
|
||||
dependencies:
|
||||
'@babel/runtime': 7.23.7
|
||||
@@ -2975,6 +3088,19 @@ snapshots:
|
||||
human-id: 1.0.2
|
||||
prettier: 2.8.8
|
||||
|
||||
'@connectrpc/connect-web@2.0.0-rc.3(@bufbuild/protobuf@2.10.0)(@connectrpc/connect@2.0.0-rc.3(@bufbuild/protobuf@2.10.0))':
|
||||
dependencies:
|
||||
'@bufbuild/protobuf': 2.10.0
|
||||
'@connectrpc/connect': 2.0.0-rc.3(@bufbuild/protobuf@2.10.0)
|
||||
|
||||
'@connectrpc/connect@2.0.0-rc.3(@bufbuild/protobuf@2.10.0)':
|
||||
dependencies:
|
||||
'@bufbuild/protobuf': 2.10.0
|
||||
|
||||
'@e2b/code-interpreter@2.2.0':
|
||||
dependencies:
|
||||
e2b: 2.6.2
|
||||
|
||||
'@esbuild/aix-ppc64@0.25.4':
|
||||
optional: true
|
||||
|
||||
@@ -3123,6 +3249,12 @@ snapshots:
|
||||
react: 18.2.0
|
||||
react-dom: 18.3.1(react@18.2.0)
|
||||
|
||||
'@isaacs/balanced-match@4.0.1': {}
|
||||
|
||||
'@isaacs/brace-expansion@5.0.0':
|
||||
dependencies:
|
||||
'@isaacs/balanced-match': 4.0.1
|
||||
|
||||
'@isaacs/cliui@8.0.2':
|
||||
dependencies:
|
||||
string-width: 5.1.2
|
||||
@@ -3132,6 +3264,10 @@ snapshots:
|
||||
wrap-ansi: 8.1.0
|
||||
wrap-ansi-cjs: wrap-ansi@7.0.0
|
||||
|
||||
'@isaacs/fs-minipass@4.0.1':
|
||||
dependencies:
|
||||
minipass: 7.1.2
|
||||
|
||||
'@jest/schemas@29.6.3':
|
||||
dependencies:
|
||||
'@sinclair/typebox': 0.27.8
|
||||
@@ -3663,6 +3799,8 @@ snapshots:
|
||||
fsevents: 2.3.3
|
||||
optional: true
|
||||
|
||||
chownr@3.0.0: {}
|
||||
|
||||
ci-info@3.9.0: {}
|
||||
|
||||
cliui@6.0.0:
|
||||
@@ -3693,6 +3831,8 @@ snapshots:
|
||||
|
||||
commander@10.0.1: {}
|
||||
|
||||
compare-versions@6.1.1: {}
|
||||
|
||||
concat-map@0.0.1: {}
|
||||
|
||||
config-chain@1.1.13:
|
||||
@@ -3793,6 +3933,11 @@ snapshots:
|
||||
dependencies:
|
||||
path-type: 4.0.0
|
||||
|
||||
dockerfile-ast@0.7.1:
|
||||
dependencies:
|
||||
vscode-languageserver-textdocument: 1.0.12
|
||||
vscode-languageserver-types: 3.17.5
|
||||
|
||||
dom-serializer@2.0.0:
|
||||
dependencies:
|
||||
domelementtype: 2.3.0
|
||||
@@ -3822,6 +3967,19 @@ snapshots:
|
||||
postgres: 3.4.7
|
||||
react: 18.2.0
|
||||
|
||||
e2b@2.6.2:
|
||||
dependencies:
|
||||
'@bufbuild/protobuf': 2.10.0
|
||||
'@connectrpc/connect': 2.0.0-rc.3(@bufbuild/protobuf@2.10.0)
|
||||
'@connectrpc/connect-web': 2.0.0-rc.3(@bufbuild/protobuf@2.10.0)(@connectrpc/connect@2.0.0-rc.3(@bufbuild/protobuf@2.10.0))
|
||||
chalk: 5.6.2
|
||||
compare-versions: 6.1.1
|
||||
dockerfile-ast: 0.7.1
|
||||
glob: 11.0.3
|
||||
openapi-fetch: 0.14.1
|
||||
platform: 1.3.6
|
||||
tar: 7.5.2
|
||||
|
||||
eastasianwidth@0.2.0: {}
|
||||
|
||||
editorconfig@1.0.4:
|
||||
@@ -4077,6 +4235,15 @@ snapshots:
|
||||
package-json-from-dist: 1.0.1
|
||||
path-scurry: 1.11.1
|
||||
|
||||
glob@11.0.3:
|
||||
dependencies:
|
||||
foreground-child: 3.3.1
|
||||
jackspeak: 4.1.1
|
||||
minimatch: 10.1.1
|
||||
minipass: 7.1.2
|
||||
package-json-from-dist: 1.0.1
|
||||
path-scurry: 2.0.0
|
||||
|
||||
glob@7.2.3:
|
||||
dependencies:
|
||||
fs.realpath: 1.0.0
|
||||
@@ -4278,6 +4445,10 @@ snapshots:
|
||||
optionalDependencies:
|
||||
'@pkgjs/parseargs': 0.11.0
|
||||
|
||||
jackspeak@4.1.1:
|
||||
dependencies:
|
||||
'@isaacs/cliui': 8.0.2
|
||||
|
||||
jose@5.10.0: {}
|
||||
|
||||
jose@6.1.0: {}
|
||||
@@ -4369,6 +4540,8 @@ snapshots:
|
||||
|
||||
lru-cache@10.4.3: {}
|
||||
|
||||
lru-cache@11.2.2: {}
|
||||
|
||||
lru-cache@4.1.5:
|
||||
dependencies:
|
||||
pseudomap: 1.0.2
|
||||
@@ -4420,6 +4593,10 @@ snapshots:
|
||||
|
||||
min-indent@1.0.1: {}
|
||||
|
||||
minimatch@10.1.1:
|
||||
dependencies:
|
||||
'@isaacs/brace-expansion': 5.0.0
|
||||
|
||||
minimatch@3.1.2:
|
||||
dependencies:
|
||||
brace-expansion: 1.1.11
|
||||
@@ -4440,6 +4617,10 @@ snapshots:
|
||||
|
||||
minipass@7.1.2: {}
|
||||
|
||||
minizlib@3.1.0:
|
||||
dependencies:
|
||||
minipass: 7.1.2
|
||||
|
||||
mixme@0.5.10: {}
|
||||
|
||||
mlly@1.4.2:
|
||||
@@ -4464,8 +4645,6 @@ snapshots:
|
||||
|
||||
nanoid@3.3.11: {}
|
||||
|
||||
nanoid@3.3.7: {}
|
||||
|
||||
nanostores@1.0.1: {}
|
||||
|
||||
needle@3.3.1:
|
||||
@@ -4509,6 +4688,12 @@ snapshots:
|
||||
dependencies:
|
||||
wrappy: 1.0.2
|
||||
|
||||
openapi-fetch@0.14.1:
|
||||
dependencies:
|
||||
openapi-typescript-helpers: 0.0.15
|
||||
|
||||
openapi-typescript-helpers@0.0.15: {}
|
||||
|
||||
os-tmpdir@1.0.2: {}
|
||||
|
||||
outdent@0.5.0: {}
|
||||
@@ -4571,6 +4756,11 @@ snapshots:
|
||||
lru-cache: 10.4.3
|
||||
minipass: 7.1.2
|
||||
|
||||
path-scurry@2.0.0:
|
||||
dependencies:
|
||||
lru-cache: 11.2.2
|
||||
minipass: 7.1.2
|
||||
|
||||
path-type@4.0.0: {}
|
||||
|
||||
pathe@1.1.1: {}
|
||||
@@ -4607,9 +4797,11 @@ snapshots:
|
||||
mlly: 1.4.2
|
||||
pathe: 1.1.1
|
||||
|
||||
platform@1.3.6: {}
|
||||
|
||||
postcss@8.4.33:
|
||||
dependencies:
|
||||
nanoid: 3.3.7
|
||||
nanoid: 3.3.11
|
||||
picocolors: 1.0.0
|
||||
source-map-js: 1.0.2
|
||||
|
||||
@@ -4974,6 +5166,14 @@ snapshots:
|
||||
react: 18.2.0
|
||||
use-sync-external-store: 1.6.0(react@18.2.0)
|
||||
|
||||
tar@7.5.2:
|
||||
dependencies:
|
||||
'@isaacs/fs-minipass': 4.0.1
|
||||
chownr: 3.0.0
|
||||
minipass: 7.1.2
|
||||
minizlib: 3.1.0
|
||||
yallist: 5.0.0
|
||||
|
||||
term-size@2.2.1: {}
|
||||
|
||||
throttleit@2.1.0: {}
|
||||
@@ -5169,6 +5369,10 @@ snapshots:
|
||||
- supports-color
|
||||
- terser
|
||||
|
||||
vscode-languageserver-textdocument@1.0.12: {}
|
||||
|
||||
vscode-languageserver-types@3.17.5: {}
|
||||
|
||||
wcwidth@1.0.1:
|
||||
dependencies:
|
||||
defaults: 1.0.4
|
||||
@@ -5253,6 +5457,8 @@ snapshots:
|
||||
|
||||
yallist@4.0.0: {}
|
||||
|
||||
yallist@5.0.0: {}
|
||||
|
||||
yargs-parser@18.1.3:
|
||||
dependencies:
|
||||
camelcase: 5.3.1
|
||||
|
||||
Reference in New Issue
Block a user