@infinitedusky/indusk-mcp 1.5.14 → 1.6.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/dist/bin/commands/extensions.js +9 -1
- package/dist/bin/commands/init.js +61 -9
- package/extensions/otel/skill.md +202 -60
- package/hooks/check-gates.js +3 -3
- package/hooks/validate-impl-structure.js +18 -5
- package/package.json +1 -1
- package/skills/plan.md +4 -1
- package/skills/toolbelt.md +71 -0
- package/skills/work.md +5 -4
- package/templates/instrumentation.next.ts +21 -0
- package/templates/instrumentation.web.ts +83 -0
|
@@ -597,9 +597,16 @@ function printMcpInstructions(name, manifest) {
|
|
|
597
597
|
];
|
|
598
598
|
const cmd = `claude ${args.map(a => a.includes(" ") ? `"${a}"` : a).join(" ")}`;
|
|
599
599
|
console.info(`\n ${name}: adding MCP server with credentials from .env...`);
|
|
600
|
+
// Remove existing entry first so we always write fresh credentials
|
|
601
|
+
try {
|
|
602
|
+
execSync(`claude mcp remove -s project ${name}`, { cwd: process.cwd(), timeout: 10000, stdio: ["ignore", "pipe", "pipe"] });
|
|
603
|
+
}
|
|
604
|
+
catch {
|
|
605
|
+
// Ignore — may not exist
|
|
606
|
+
}
|
|
600
607
|
try {
|
|
601
608
|
execSync(cmd, { cwd: process.cwd(), timeout: 15000, stdio: ["ignore", "pipe", "pipe"] });
|
|
602
|
-
console.info(` ${name}: MCP server
|
|
609
|
+
console.info(` ${name}: MCP server configured (restart Claude Code to load)`);
|
|
603
610
|
return;
|
|
604
611
|
}
|
|
605
612
|
catch (e) {
|
|
@@ -607,6 +614,7 @@ function printMcpInstructions(name, manifest) {
|
|
|
607
614
|
const stderr = err.stderr ? String(err.stderr).trim() : "";
|
|
608
615
|
console.info(` ${name}: auto-add failed. ${stderr || err.message || ""}`);
|
|
609
616
|
console.info(` command was: ${cmd}`);
|
|
617
|
+
return;
|
|
610
618
|
}
|
|
611
619
|
}
|
|
612
620
|
}
|
|
@@ -190,6 +190,9 @@ export async function init(projectRoot, options = {}) {
|
|
|
190
190
|
existsSync(join(projectRoot, "next.config.mjs"));
|
|
191
191
|
const isPython = existsSync(join(projectRoot, "requirements.txt")) ||
|
|
192
192
|
existsSync(join(projectRoot, "pyproject.toml"));
|
|
193
|
+
const isReactSPA = !isNextJs &&
|
|
194
|
+
(existsSync(join(projectRoot, "vite.config.ts")) ||
|
|
195
|
+
existsSync(join(projectRoot, "vite.config.js")));
|
|
193
196
|
if (isPython) {
|
|
194
197
|
const pyTemplate = "instrumentation.py";
|
|
195
198
|
const targetFile = join(projectRoot, pyTemplate);
|
|
@@ -203,9 +206,64 @@ export async function init(projectRoot, options = {}) {
|
|
|
203
206
|
console.info(" run: opentelemetry-instrument python your_app.py");
|
|
204
207
|
}
|
|
205
208
|
}
|
|
209
|
+
else if (isNextJs) {
|
|
210
|
+
// Next.js — use @vercel/otel
|
|
211
|
+
const instrTarget = join(projectRoot, "instrumentation.ts");
|
|
212
|
+
if (existsSync(instrTarget) && !force) {
|
|
213
|
+
console.info(" skip: instrumentation.ts (already exists)");
|
|
214
|
+
}
|
|
215
|
+
else {
|
|
216
|
+
cpSync(join(packageRoot, "templates/instrumentation.next.ts"), instrTarget);
|
|
217
|
+
const content = readFileSync(instrTarget, "utf-8");
|
|
218
|
+
writeFileSync(instrTarget, content.replace('"unknown-service"', `"${projectName}"`));
|
|
219
|
+
console.info(" create: instrumentation.ts (Next.js — @vercel/otel)");
|
|
220
|
+
}
|
|
221
|
+
// Logger
|
|
222
|
+
const loggerTarget = join(projectRoot, "src/logger.ts");
|
|
223
|
+
if (existsSync(loggerTarget) && !force) {
|
|
224
|
+
console.info(" skip: src/logger.ts (already exists)");
|
|
225
|
+
}
|
|
226
|
+
else {
|
|
227
|
+
const loggerDir = join(projectRoot, "src");
|
|
228
|
+
mkdirSync(loggerDir, { recursive: true });
|
|
229
|
+
cpSync(join(packageRoot, "templates/logger.ts"), loggerTarget);
|
|
230
|
+
console.info(" create: src/logger.ts");
|
|
231
|
+
}
|
|
232
|
+
// Client-side browser instrumentation
|
|
233
|
+
const webInstrTarget = join(projectRoot, "src/instrumentation.web.ts");
|
|
234
|
+
if (existsSync(webInstrTarget) && !force) {
|
|
235
|
+
console.info(" skip: src/instrumentation.web.ts (already exists)");
|
|
236
|
+
}
|
|
237
|
+
else {
|
|
238
|
+
const srcDir = join(projectRoot, "src");
|
|
239
|
+
mkdirSync(srcDir, { recursive: true });
|
|
240
|
+
cpSync(join(packageRoot, "templates/instrumentation.web.ts"), webInstrTarget);
|
|
241
|
+
const content = readFileSync(webInstrTarget, "utf-8");
|
|
242
|
+
writeFileSync(webInstrTarget, content.replace('"unknown-service"', `"${projectName}"`));
|
|
243
|
+
console.info(" create: src/instrumentation.web.ts (browser — OTel Web SDK)");
|
|
244
|
+
}
|
|
245
|
+
console.info(" install: pnpm add @vercel/otel pino pino-opentelemetry-transport @opentelemetry/sdk-trace-web @opentelemetry/sdk-trace-base @opentelemetry/exporter-trace-otlp-http @opentelemetry/resources @opentelemetry/semantic-conventions @opentelemetry/instrumentation @opentelemetry/instrumentation-fetch @opentelemetry/instrumentation-document-load @opentelemetry/instrumentation-user-interaction");
|
|
246
|
+
console.info(" wire (server): Next.js loads instrumentation.ts automatically");
|
|
247
|
+
console.info(" wire (client): import './instrumentation.web' in your root client component or layout");
|
|
248
|
+
}
|
|
249
|
+
else if (isReactSPA) {
|
|
250
|
+
// React SPA (Vite) — browser instrumentation via Dash0 Web SDK
|
|
251
|
+
const srcDir = existsSync(join(projectRoot, "src")) ? join(projectRoot, "src") : projectRoot;
|
|
252
|
+
const instrTarget = join(srcDir, "instrumentation.ts");
|
|
253
|
+
if (existsSync(instrTarget) && !force) {
|
|
254
|
+
console.info(" skip: instrumentation.ts (already exists)");
|
|
255
|
+
}
|
|
256
|
+
else {
|
|
257
|
+
cpSync(join(packageRoot, "templates/instrumentation.web.ts"), instrTarget);
|
|
258
|
+
const content = readFileSync(instrTarget, "utf-8");
|
|
259
|
+
writeFileSync(instrTarget, content.replace('"unknown-service"', `"${projectName}"`));
|
|
260
|
+
console.info(" create: src/instrumentation.ts (React SPA — @dash0/sdk-web)");
|
|
261
|
+
}
|
|
262
|
+
console.info(" install: pnpm add @opentelemetry/sdk-trace-web @opentelemetry/sdk-trace-base @opentelemetry/exporter-trace-otlp-http @opentelemetry/resources @opentelemetry/semantic-conventions @opentelemetry/instrumentation @opentelemetry/instrumentation-fetch @opentelemetry/instrumentation-document-load @opentelemetry/instrumentation-user-interaction");
|
|
263
|
+
console.info(" wire: import './instrumentation' at the top of your main.tsx or App.tsx");
|
|
264
|
+
}
|
|
206
265
|
else {
|
|
207
|
-
// Node.js
|
|
208
|
-
const targetDir = isNextJs ? projectRoot : join(projectRoot, "src");
|
|
266
|
+
// Node.js — full SDK with auto-instrumentation
|
|
209
267
|
const srcDir = existsSync(join(projectRoot, "src")) ? join(projectRoot, "src") : projectRoot;
|
|
210
268
|
for (const template of otelTemplates) {
|
|
211
269
|
const targetFile = join(srcDir, template);
|
|
@@ -222,7 +280,6 @@ export async function init(projectRoot, options = {}) {
|
|
|
222
280
|
const content = readFileSync(instrPath, "utf-8");
|
|
223
281
|
writeFileSync(instrPath, content.replace('"unknown-service"', `"${projectName}"`));
|
|
224
282
|
}
|
|
225
|
-
// Print instructions
|
|
226
283
|
const packages = [
|
|
227
284
|
"@opentelemetry/sdk-node",
|
|
228
285
|
"@opentelemetry/auto-instrumentations-node",
|
|
@@ -235,12 +292,7 @@ export async function init(projectRoot, options = {}) {
|
|
|
235
292
|
"pino-opentelemetry-transport",
|
|
236
293
|
];
|
|
237
294
|
console.info(` install: pnpm add ${packages.join(" ")}`);
|
|
238
|
-
|
|
239
|
-
console.info(" wire: Next.js instrumentation hook (instrumentation.ts in app root)");
|
|
240
|
-
}
|
|
241
|
-
else {
|
|
242
|
-
console.info(" wire: node -r ./src/instrumentation.ts src/index.ts");
|
|
243
|
-
}
|
|
295
|
+
console.info(" wire: node --import ./src/instrumentation.ts src/index.ts");
|
|
244
296
|
}
|
|
245
297
|
} // end isInduskMcp else
|
|
246
298
|
// 8. Install gate enforcement hooks
|
package/extensions/otel/skill.md
CHANGED
|
@@ -1,13 +1,24 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: otel
|
|
3
|
+
description: OpenTelemetry instrumentation patterns — span naming, categories, structured logging, error propagation, sensitive data, and validation
|
|
4
|
+
---
|
|
5
|
+
|
|
1
6
|
# OpenTelemetry
|
|
2
7
|
|
|
3
|
-
Every service is instrumented from day one. `init` creates the instrumentation files. This skill teaches you how to
|
|
8
|
+
Every service is instrumented from day one. `init` creates the instrumentation files. This skill teaches you how to produce high-quality telemetry.
|
|
4
9
|
|
|
5
10
|
## What's Already Set Up
|
|
6
11
|
|
|
7
|
-
After `init`, your project has
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
12
|
+
After `init`, your project has instrumentation files for your runtime. See the OTel reference docs in the indusk-docs site for the full list per runtime.
|
|
13
|
+
|
|
14
|
+
## Core Principle: Signal Density Over Volume
|
|
15
|
+
|
|
16
|
+
Every telemetry item should serve one of three purposes:
|
|
17
|
+
- **Detect** — help identify that something is wrong
|
|
18
|
+
- **Localize** — help pinpoint where the problem is
|
|
19
|
+
- **Explain** — help understand why it happened
|
|
20
|
+
|
|
21
|
+
If it doesn't serve one of these purposes, don't emit it.
|
|
11
22
|
|
|
12
23
|
## When to Add Manual Spans
|
|
13
24
|
|
|
@@ -22,20 +33,61 @@ If it's a meaningful action that you'd want to see in a trace, add a span.
|
|
|
22
33
|
|
|
23
34
|
## Span Naming
|
|
24
35
|
|
|
25
|
-
|
|
36
|
+
Span names MUST be low-cardinality. The number of unique span names should be bounded and small.
|
|
26
37
|
|
|
27
|
-
|
|
28
|
-
const tracer = trace.getTracer('my-service');
|
|
38
|
+
### General pattern: `{domain}.{entity}.{action}`
|
|
29
39
|
|
|
30
|
-
|
|
40
|
+
```typescript
|
|
41
|
+
// Good — low cardinality
|
|
31
42
|
tracer.startSpan('poker.hand.deal');
|
|
32
43
|
tracer.startSpan('auth.session.create');
|
|
33
44
|
tracer.startSpan('settlement.receipt.sign');
|
|
34
45
|
|
|
35
|
-
// Bad
|
|
36
|
-
tracer.startSpan(
|
|
37
|
-
tracer.startSpan(
|
|
38
|
-
|
|
46
|
+
// Bad — high cardinality (contains variable data)
|
|
47
|
+
tracer.startSpan(`process_payment_for_${userId}`); // user ID in name!
|
|
48
|
+
tracer.startSpan(`GET /api/users/${id}`); // path param in name!
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Variable data (user IDs, order numbers, room codes) goes in **attributes**, never in the span name.
|
|
52
|
+
|
|
53
|
+
### Per-signal naming
|
|
54
|
+
|
|
55
|
+
| Signal | Format | Example |
|
|
56
|
+
|--------|--------|---------|
|
|
57
|
+
| HTTP server | `{method} {route}` | `GET /api/users/:id` |
|
|
58
|
+
| HTTP client | `{method} {template}` | `POST /checkout` |
|
|
59
|
+
| Database | `{operation} {collection}` | `SELECT orders` |
|
|
60
|
+
| Messaging | `{operation} {destination}` | `publish shop.orders` |
|
|
61
|
+
| Business logic | `{domain}.{entity}.{action}` | `poker.hand.deal` |
|
|
62
|
+
|
|
63
|
+
## Span Kind
|
|
64
|
+
|
|
65
|
+
Each span has exactly one kind. Choose based on the communication pattern:
|
|
66
|
+
|
|
67
|
+
| Kind | Use When | Examples |
|
|
68
|
+
|------|----------|---------|
|
|
69
|
+
| `SERVER` | Handling an inbound sync request | HTTP handler, gRPC handler |
|
|
70
|
+
| `CLIENT` | Making an outbound sync request | HTTP call, database query, outbound RPC |
|
|
71
|
+
| `PRODUCER` | Initiating an async operation | Publishing to a queue |
|
|
72
|
+
| `CONSUMER` | Processing an async operation | Processing a queued message |
|
|
73
|
+
| `INTERNAL` | Internal operation, no remote peer | In-memory computation, business logic |
|
|
74
|
+
|
|
75
|
+
Common mistakes:
|
|
76
|
+
- Using `INTERNAL` for everything — database calls are `CLIENT`, HTTP handlers are `SERVER`
|
|
77
|
+
- Using `CLIENT` for message publishing — that's `PRODUCER` (async, no response expected)
|
|
78
|
+
|
|
79
|
+
## Span Status
|
|
80
|
+
|
|
81
|
+
Only set error status explicitly. Success is the default.
|
|
82
|
+
|
|
83
|
+
```typescript
|
|
84
|
+
import { SpanStatusCode } from '@opentelemetry/api';
|
|
85
|
+
|
|
86
|
+
// Only set on error
|
|
87
|
+
span.setStatus({ code: SpanStatusCode.ERROR, message: err.message });
|
|
88
|
+
|
|
89
|
+
// Don't set OK explicitly — it's the default
|
|
90
|
+
// span.setStatus({ code: SpanStatusCode.OK }); // unnecessary
|
|
39
91
|
```
|
|
40
92
|
|
|
41
93
|
## Custom Attributes
|
|
@@ -43,38 +95,43 @@ tracer.startSpan('doThing');
|
|
|
43
95
|
Always add domain-specific data to spans:
|
|
44
96
|
|
|
45
97
|
```typescript
|
|
46
|
-
const span = tracer.startSpan('poker.hand.deal'
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
98
|
+
const span = tracer.startSpan('poker.hand.deal', {
|
|
99
|
+
attributes: {
|
|
100
|
+
'otel.category': 'business',
|
|
101
|
+
'room.code': roomCode,
|
|
102
|
+
'hand.number': handNumber,
|
|
103
|
+
'player.count': players.length,
|
|
104
|
+
},
|
|
105
|
+
});
|
|
53
106
|
```
|
|
54
107
|
|
|
108
|
+
### Attribute hygiene
|
|
109
|
+
|
|
110
|
+
- Use snake_case with dots for namespacing: `room.code`, `player.count`
|
|
111
|
+
- Keep values low-cardinality where possible
|
|
112
|
+
- Never put variable-length user input in attributes without truncation
|
|
113
|
+
|
|
55
114
|
## Categories
|
|
56
115
|
|
|
57
|
-
Every manual span should get an `otel.category` attribute
|
|
116
|
+
Every manual span should get an `otel.category` attribute:
|
|
58
117
|
|
|
59
118
|
| Category | What it covers | Examples |
|
|
60
119
|
|----------|---------------|----------|
|
|
61
|
-
| `http` | HTTP
|
|
120
|
+
| `http` | HTTP requests | Auto-instrumented |
|
|
62
121
|
| `db` | Database queries | Auto-instrumented |
|
|
63
122
|
| `business` | Domain events | hand dealt, user registered, payment processed |
|
|
64
123
|
| `inference` | LLM/AI calls | Gemini generate, embedding create |
|
|
65
|
-
| `state` | State transitions | game started, order
|
|
66
|
-
| `system` | Infrastructure | health checks, cron
|
|
124
|
+
| `state` | State transitions | game started, order shipped |
|
|
125
|
+
| `system` | Infrastructure | health checks, cron, queue processing |
|
|
67
126
|
|
|
68
|
-
Control
|
|
127
|
+
Control export via `OTEL_ENABLED_CATEGORIES`:
|
|
69
128
|
|
|
70
129
|
```bash
|
|
71
|
-
#
|
|
72
|
-
OTEL_ENABLED_CATEGORIES=http,business node -r ./instrumentation.ts src/index.ts
|
|
73
|
-
|
|
74
|
-
# Export everything (default)
|
|
75
|
-
node -r ./instrumentation.ts src/index.ts
|
|
130
|
+
OTEL_ENABLED_CATEGORIES=http,business # only these categories exported
|
|
76
131
|
```
|
|
77
132
|
|
|
133
|
+
Unset = all categories exported. Spans without a category are always exported.
|
|
134
|
+
|
|
78
135
|
## Structured Logging with Pino
|
|
79
136
|
|
|
80
137
|
Use the project logger, not `console.log`:
|
|
@@ -84,70 +141,155 @@ import { logger } from './logger';
|
|
|
84
141
|
|
|
85
142
|
// Good — structured, with context
|
|
86
143
|
logger.info({ roomCode, players: players.length }, 'hand started');
|
|
87
|
-
logger.error({ err,
|
|
88
|
-
logger.warn({ queueDepth: 150, threshold: 100 }, 'queue depth approaching limit');
|
|
144
|
+
logger.error({ err, orderId }, 'payment failed');
|
|
89
145
|
|
|
90
146
|
// Bad — unstructured string
|
|
91
147
|
console.log('Hand started for room ' + roomCode);
|
|
92
|
-
console.log('Error: ' + err.message);
|
|
93
148
|
```
|
|
94
149
|
|
|
95
|
-
### Log
|
|
150
|
+
### Log levels
|
|
96
151
|
|
|
97
152
|
| Level | Meaning | When to use |
|
|
98
153
|
|-------|---------|------------|
|
|
99
|
-
| `error` | Something is broken | Unhandled errors, failed operations
|
|
100
|
-
| `warn` | Degraded but functional | Approaching limits, fallback behavior
|
|
101
|
-
| `info` | Business events | State transitions,
|
|
102
|
-
| `debug` | Development details |
|
|
154
|
+
| `error` | Something is broken | Unhandled errors, failed operations |
|
|
155
|
+
| `warn` | Degraded but functional | Approaching limits, fallback behavior |
|
|
156
|
+
| `info` | Business events | State transitions, completed operations |
|
|
157
|
+
| `debug` | Development details | Disable in production |
|
|
158
|
+
|
|
159
|
+
### Structured logging safeguards
|
|
160
|
+
|
|
161
|
+
Never spread entire objects — explicitly pick safe fields:
|
|
162
|
+
|
|
163
|
+
```typescript
|
|
164
|
+
// BAD: spreads entire request body — may contain passwords, tokens, PII
|
|
165
|
+
logger.info({ ...req.body }, 'user signup');
|
|
166
|
+
|
|
167
|
+
// GOOD: explicitly select safe fields
|
|
168
|
+
logger.info({ userId: req.body.userId, plan: req.body.plan }, 'user signup');
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
## Trace Correlation in Logs
|
|
172
|
+
|
|
173
|
+
Every log inside an active span should carry trace context:
|
|
174
|
+
|
|
175
|
+
```typescript
|
|
176
|
+
import { trace, context } from '@opentelemetry/api';
|
|
177
|
+
|
|
178
|
+
function getTraceContext() {
|
|
179
|
+
const span = trace.getSpan(context.active());
|
|
180
|
+
if (!span) return {};
|
|
181
|
+
const ctx = span.spanContext();
|
|
182
|
+
return { traceId: ctx.traceId, spanId: ctx.spanId };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
logger.info({ ...getTraceContext(), orderId }, 'order placed');
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
Wrap this in a helper so every log call includes trace context automatically. Without it, logs are isolated events that can't be connected to the request that produced them.
|
|
103
189
|
|
|
104
190
|
## Error Propagation
|
|
105
191
|
|
|
106
|
-
Errors must always include trace context:
|
|
192
|
+
Errors must always include trace context. Never swallow silently:
|
|
107
193
|
|
|
108
194
|
```typescript
|
|
109
195
|
try {
|
|
110
196
|
await processSettlement(hand);
|
|
111
197
|
} catch (err) {
|
|
198
|
+
// 1. Record on span
|
|
112
199
|
span.recordException(err);
|
|
113
200
|
span.setStatus({ code: SpanStatusCode.ERROR, message: err.message });
|
|
114
|
-
|
|
115
|
-
|
|
201
|
+
|
|
202
|
+
// 2. Log with correlation
|
|
203
|
+
logger.error({ err, ...getTraceContext() }, 'settlement failed');
|
|
204
|
+
|
|
205
|
+
// 3. Re-throw — don't swallow
|
|
206
|
+
throw err;
|
|
116
207
|
}
|
|
117
208
|
```
|
|
118
209
|
|
|
119
|
-
|
|
120
|
-
1. Re-throw with context
|
|
210
|
+
Every catch block should either:
|
|
211
|
+
1. Re-throw with context preserved
|
|
121
212
|
2. Log with trace correlation and handle completely
|
|
122
213
|
|
|
214
|
+
## Sensitive Data
|
|
215
|
+
|
|
216
|
+
Never attach these to spans, logs, or metrics:
|
|
217
|
+
|
|
218
|
+
| Category | Examples |
|
|
219
|
+
|----------|----------|
|
|
220
|
+
| Auth credentials | Passwords, API keys, bearer tokens, session cookies |
|
|
221
|
+
| Financial instruments | Credit card numbers, bank accounts, CVVs |
|
|
222
|
+
| Government IDs | SSN, passport numbers, tax IDs |
|
|
223
|
+
| Full auth headers | `Authorization`, `Cookie` header values |
|
|
224
|
+
|
|
225
|
+
For URLs, strip query parameters that carry tokens or user input:
|
|
226
|
+
|
|
227
|
+
```typescript
|
|
228
|
+
// BAD
|
|
229
|
+
span.setAttribute('http.url', 'https://example.com/callback?token=eyJhbG...');
|
|
230
|
+
|
|
231
|
+
// GOOD
|
|
232
|
+
span.setAttribute('http.url', sanitizeUrl(req.url));
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
For database queries, never include literal parameter values — use parameterized queries.
|
|
236
|
+
|
|
237
|
+
## Validation Checklist
|
|
238
|
+
|
|
239
|
+
After instrumenting a service, verify:
|
|
240
|
+
|
|
241
|
+
1. **Service appears in backend** — search for your `OTEL_SERVICE_NAME` in Dash0
|
|
242
|
+
2. **Resource attributes present** — `service.name`, `service.version`, `deployment.environment`
|
|
243
|
+
3. **Expected span names appear** — list your endpoints and business operations, verify each produces a span
|
|
244
|
+
4. **Attributes are populated** — check that domain attributes (`room.code`, `player.count`) are present
|
|
245
|
+
5. **Errors are tracked** — trigger an error, verify it appears with exception details and trace context
|
|
246
|
+
6. **Logs correlate with traces** — verify `traceId` and `spanId` appear in log records
|
|
247
|
+
|
|
123
248
|
## Framework-Specific Notes
|
|
124
249
|
|
|
250
|
+
### Node.js (ESM)
|
|
251
|
+
- Load instrumentation with `--import`: `node --import ./src/instrumentation.ts src/index.ts`
|
|
252
|
+
- ESM requires `--import`, not `--require`
|
|
253
|
+
|
|
125
254
|
### Next.js
|
|
126
|
-
|
|
255
|
+
- Server: `instrumentation.ts` at app root, loaded automatically by Next.js (13.4+)
|
|
256
|
+
- Client: `src/instrumentation.web.ts`, import in your root client component
|
|
257
|
+
- Both server and client need separate instrumentation — they run in different environments
|
|
258
|
+
|
|
259
|
+
### React SPA (Vite)
|
|
260
|
+
- `import './instrumentation'` at the top of `main.tsx`
|
|
261
|
+
- Browser env vars use `VITE_` prefix: `VITE_OTEL_EXPORTER_OTLP_ENDPOINT`
|
|
127
262
|
|
|
128
263
|
### Python
|
|
129
|
-
Use
|
|
130
|
-
|
|
131
|
-
opentelemetry-instrument python your_app.py
|
|
132
|
-
```
|
|
133
|
-
Or import the instrumentation module before your app code.
|
|
264
|
+
- Use `opentelemetry-instrument` CLI: `opentelemetry-instrument python your_app.py`
|
|
265
|
+
- Or import `instrumentation.py` before app code
|
|
134
266
|
|
|
135
267
|
## Environment Variables
|
|
136
268
|
|
|
269
|
+
### Server
|
|
270
|
+
|
|
137
271
|
| Variable | Purpose | Default |
|
|
138
272
|
|----------|---------|---------|
|
|
139
|
-
| `OTEL_SERVICE_NAME` | Service name
|
|
140
|
-
| `OTEL_EXPORTER_OTLP_ENDPOINT` | Backend URL | Not set (
|
|
141
|
-
| `OTEL_EXPORTER_OTLP_HEADERS` | Auth headers
|
|
142
|
-
| `OTEL_ENABLED_CATEGORIES` |
|
|
273
|
+
| `OTEL_SERVICE_NAME` | Service name | Set by init |
|
|
274
|
+
| `OTEL_EXPORTER_OTLP_ENDPOINT` | Backend URL | Not set (console) |
|
|
275
|
+
| `OTEL_EXPORTER_OTLP_HEADERS` | Auth headers | Not set |
|
|
276
|
+
| `OTEL_ENABLED_CATEGORIES` | Active categories | All |
|
|
143
277
|
| `LOG_LEVEL` | Pino log level | `info` |
|
|
144
278
|
|
|
145
|
-
|
|
279
|
+
### Browser
|
|
280
|
+
|
|
281
|
+
| Variable | Purpose |
|
|
282
|
+
|----------|---------|
|
|
283
|
+
| `VITE_OTEL_EXPORTER_OTLP_ENDPOINT` / `NEXT_PUBLIC_OTEL_EXPORTER_OTLP_ENDPOINT` | Backend URL |
|
|
284
|
+
| `VITE_OTEL_EXPORTER_OTLP_HEADERS` / `NEXT_PUBLIC_OTEL_EXPORTER_OTLP_HEADERS` | Auth headers |
|
|
146
285
|
|
|
147
|
-
##
|
|
286
|
+
## During `/work`
|
|
148
287
|
|
|
149
|
-
|
|
150
|
-
- Are new endpoints
|
|
151
|
-
- Do
|
|
152
|
-
-
|
|
153
|
-
-
|
|
288
|
+
When implementing or reviewing code, check:
|
|
289
|
+
- Are new endpoints and business logic instrumented with manual spans?
|
|
290
|
+
- Do spans have meaningful names (low-cardinality, `{domain}.{entity}.{action}`)?
|
|
291
|
+
- Do spans have the right kind (SERVER, CLIENT, INTERNAL)?
|
|
292
|
+
- Do spans have `otel.category` and domain-specific attributes?
|
|
293
|
+
- Are errors recorded with `recordException` + `setStatus(ERROR)` + trace-correlated log?
|
|
294
|
+
- Is structured logging used (not `console.log`)?
|
|
295
|
+
- Is sensitive data excluded from attributes and logs?
|
package/hooks/check-gates.js
CHANGED
|
@@ -117,8 +117,8 @@ function detectWorkflow(content) {
|
|
|
117
117
|
|
|
118
118
|
// Which gate types are required per workflow
|
|
119
119
|
const WORKFLOW_GATES = {
|
|
120
|
-
feature: ["verification", "context", "document"],
|
|
121
|
-
refactor: ["verification", "context", "document"],
|
|
120
|
+
feature: ["verification", "otel", "context", "document"],
|
|
121
|
+
refactor: ["verification", "otel", "context", "document"],
|
|
122
122
|
bugfix: ["verification", "document"],
|
|
123
123
|
spike: [],
|
|
124
124
|
};
|
|
@@ -147,7 +147,7 @@ function parsePhases(content) {
|
|
|
147
147
|
continue;
|
|
148
148
|
}
|
|
149
149
|
|
|
150
|
-
const gateMatch = line.match(/^####\s+Phase\s+\d+\s+(Verification|Context|Document)\b/);
|
|
150
|
+
const gateMatch = line.match(/^####\s+Phase\s+\d+\s+(Verification|OTel|Context|Document)\b/);
|
|
151
151
|
if (gateMatch) {
|
|
152
152
|
currentGateType = gateMatch[1].toLowerCase();
|
|
153
153
|
continue;
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* PreToolUse hook: validates that impl phases have all four gate sections.
|
|
4
4
|
*
|
|
5
|
-
* Every phase must have: implementation items, Verification, Context, Document.
|
|
5
|
+
* Every phase must have: implementation items, Verification, OTel, Context, Document.
|
|
6
6
|
* Sections can opt out with (none needed), (not applicable), or skip-reason: {why}.
|
|
7
7
|
*
|
|
8
8
|
* Exit 0 = allow the edit
|
|
@@ -102,10 +102,10 @@ const workflow = workflowMatch ? workflowMatch[1] : "feature";
|
|
|
102
102
|
|
|
103
103
|
// Different workflows have different requirements
|
|
104
104
|
const requirements = {
|
|
105
|
-
feature: { verification: true, context: true, document: true },
|
|
106
|
-
refactor: { verification: true, context: true, document: true },
|
|
107
|
-
bugfix: { verification: true, context: false, document: true },
|
|
108
|
-
spike: { verification: false, context: false, document: false },
|
|
105
|
+
feature: { verification: true, otel: true, context: true, document: true },
|
|
106
|
+
refactor: { verification: true, otel: true, context: true, document: true },
|
|
107
|
+
bugfix: { verification: true, otel: false, context: false, document: true },
|
|
108
|
+
spike: { verification: false, otel: false, context: false, document: false },
|
|
109
109
|
}[workflow];
|
|
110
110
|
const lines = body.split("\n");
|
|
111
111
|
|
|
@@ -122,9 +122,11 @@ for (const line of lines) {
|
|
|
122
122
|
name: phaseMatch[2].trim(),
|
|
123
123
|
hasImplementation: false,
|
|
124
124
|
hasVerification: false,
|
|
125
|
+
hasOtel: false,
|
|
125
126
|
hasContext: false,
|
|
126
127
|
hasDocument: false,
|
|
127
128
|
verificationIsOptOut: false,
|
|
129
|
+
otelIsOptOut: false,
|
|
128
130
|
contextIsOptOut: false,
|
|
129
131
|
documentIsOptOut: false,
|
|
130
132
|
};
|
|
@@ -142,6 +144,13 @@ for (const line of lines) {
|
|
|
142
144
|
continue;
|
|
143
145
|
}
|
|
144
146
|
|
|
147
|
+
const otelMatch = line.match(/^####\s+Phase\s+\d+\s+OTel\b/);
|
|
148
|
+
if (otelMatch) {
|
|
149
|
+
currentPhase.hasOtel = true;
|
|
150
|
+
currentSection = "otel";
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
|
|
145
154
|
const ctxMatch = line.match(/^####\s+Phase\s+\d+\s+Context\b/);
|
|
146
155
|
if (ctxMatch) {
|
|
147
156
|
currentPhase.hasContext = true;
|
|
@@ -168,6 +177,7 @@ for (const line of lines) {
|
|
|
168
177
|
line.includes("skip-reason:");
|
|
169
178
|
if (currentPhase && isOptOutLine) {
|
|
170
179
|
if (currentSection === "verification") currentPhase.verificationIsOptOut = true;
|
|
180
|
+
if (currentSection === "otel") currentPhase.otelIsOptOut = true;
|
|
171
181
|
if (currentSection === "context") currentPhase.contextIsOptOut = true;
|
|
172
182
|
if (currentSection === "document") currentPhase.documentIsOptOut = true;
|
|
173
183
|
}
|
|
@@ -186,6 +196,7 @@ for (const phase of phases) {
|
|
|
186
196
|
|
|
187
197
|
const missing = [];
|
|
188
198
|
if (requirements.verification && !phase.hasVerification) missing.push("Verification");
|
|
199
|
+
if (requirements.otel && !phase.hasOtel) missing.push("OTel");
|
|
189
200
|
if (requirements.context && !phase.hasContext) missing.push("Context");
|
|
190
201
|
if (requirements.document && !phase.hasDocument) missing.push("Document");
|
|
191
202
|
|
|
@@ -200,6 +211,8 @@ for (const phase of phases) {
|
|
|
200
211
|
const optOuts = [];
|
|
201
212
|
if (requirements.verification && phase.hasVerification && phase.verificationIsOptOut)
|
|
202
213
|
optOuts.push("Verification");
|
|
214
|
+
if (requirements.otel && phase.hasOtel && phase.otelIsOptOut)
|
|
215
|
+
optOuts.push("OTel");
|
|
203
216
|
if (requirements.context && phase.hasContext && phase.contextIsOptOut) optOuts.push("Context");
|
|
204
217
|
if (requirements.document && phase.hasDocument && phase.documentIsOptOut)
|
|
205
218
|
optOuts.push("Document");
|
package/package.json
CHANGED
package/skills/plan.md
CHANGED
|
@@ -256,8 +256,11 @@ For multi-phase impls, include a boundary map showing what each phase produces a
|
|
|
256
256
|
function withdrawFor(wallet: address, player: address, amount: uint256, historyHash: bytes32)
|
|
257
257
|
```
|
|
258
258
|
|
|
259
|
+
#### Phase 1 OTel
|
|
260
|
+
- [ ] {Instrumentation check — are new code paths observable? See the OTel skill for patterns. Example items: "New endpoints have manual spans with `otel.category` and domain attributes", "Errors recorded with `recordException` + `setStatus(ERROR)` + trace-correlated log". Ask: "did this phase add endpoints, business logic, state transitions, or error paths?" If not, this section can be opted out per gate policy.}
|
|
261
|
+
|
|
259
262
|
#### Phase 1 Verification
|
|
260
|
-
- [ ] {Verification step — prove this phase works. Must be a specific runnable command with expected output, not "verify it works." See the verify skill for guidance on what checks a phase needs based on what changed.}
|
|
263
|
+
- [ ] {Verification step — prove this phase works. Must be a specific runnable command with expected output, not "verify it works." See the verify skill for guidance on what checks a phase needs based on what changed. Can include trace verification if OTel was added.}
|
|
261
264
|
|
|
262
265
|
#### Phase 1 Context
|
|
263
266
|
- [ ] {Concrete CLAUDE.md edit this phase produces — e.g., "Add to Architecture: ...", "Add to Conventions: ...", "Update Current State: ...". Ask: "what does this phase change about how the project works?" If nothing, omit this section.}
|
package/skills/toolbelt.md
CHANGED
|
@@ -48,6 +48,77 @@ When a new session begins:
|
|
|
48
48
|
3. Call `list_plans` — understand what plans exist, their stages, and what's in progress.
|
|
49
49
|
4. Call `get_context` — read the project's CLAUDE.md to understand architecture, conventions, and current state.
|
|
50
50
|
|
|
51
|
+
## Creating a New App
|
|
52
|
+
|
|
53
|
+
When starting a new service or app in the monorepo, follow this sequence:
|
|
54
|
+
|
|
55
|
+
### 1. Create the app
|
|
56
|
+
|
|
57
|
+
| Runtime | Command |
|
|
58
|
+
|---------|---------|
|
|
59
|
+
| **Next.js** | `npx create-next-app apps/my-app` |
|
|
60
|
+
| **React SPA** (Vite) | `pnpm create vite apps/my-app --template react-ts` |
|
|
61
|
+
| **Node.js** (Express/Fastify) | Create `apps/my-app/` with `package.json`, `src/index.ts` |
|
|
62
|
+
| **Python** | Create `apps/my-app/` with `pyproject.toml` or `requirements.txt` |
|
|
63
|
+
|
|
64
|
+
### 2. Initialize indusk-mcp
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
cd apps/my-app
|
|
68
|
+
npx @infinitedusky/indusk-mcp init
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
This detects the runtime and scaffolds:
|
|
72
|
+
- Skills, hooks, lessons, CLAUDE.md
|
|
73
|
+
- **Next.js**: `instrumentation.ts` (app root) using `@vercel/otel`, `src/logger.ts` using Pino
|
|
74
|
+
- **Node.js**: `src/instrumentation.ts` (full OTel SDK + FilteringExporter), `src/filtering-exporter.ts`, `src/logger.ts`
|
|
75
|
+
- **Python**: `instrumentation.py` (OTel SDK with auto-instrumentation)
|
|
76
|
+
- Biome config, VS Code settings, `.cgcignore`
|
|
77
|
+
|
|
78
|
+
### 3. Install the printed packages
|
|
79
|
+
|
|
80
|
+
`init` prints the install command — run it:
|
|
81
|
+
- **Next.js**: `pnpm add @vercel/otel pino pino-opentelemetry-transport`
|
|
82
|
+
- **Node.js**: `pnpm add @opentelemetry/sdk-node @opentelemetry/auto-instrumentations-node ...` (full list printed by init)
|
|
83
|
+
- **Python**: `pip install opentelemetry-distro opentelemetry-instrumentation opentelemetry-exporter-otlp`
|
|
84
|
+
|
|
85
|
+
### 4. Wire instrumentation into the entry point
|
|
86
|
+
|
|
87
|
+
- **Next.js**: automatic — Next.js loads `instrumentation.ts` from the app root
|
|
88
|
+
- **Node.js**: `node --import ./src/instrumentation.ts src/index.ts`
|
|
89
|
+
- **Python**: `opentelemetry-instrument python your_app.py`
|
|
90
|
+
|
|
91
|
+
### 5. Create a composable.env contract (if using composable.env)
|
|
92
|
+
|
|
93
|
+
Create `env/contracts/my-app.contract.json`:
|
|
94
|
+
```json
|
|
95
|
+
{
|
|
96
|
+
"name": "my-app",
|
|
97
|
+
"location": "apps/my-app",
|
|
98
|
+
"vars": {
|
|
99
|
+
"OTEL_SERVICE_NAME": "my-app",
|
|
100
|
+
"OTEL_EXPORTER_OTLP_ENDPOINT": "${dash0.HTTP_ENDPOINT}",
|
|
101
|
+
"OTEL_EXPORTER_OTLP_HEADERS": "${dash0.OTLP_HEADERS}"
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Then `pnpm ce env:build` generates the `.env` files.
|
|
107
|
+
|
|
108
|
+
### 6. Enable extensions
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
npx @infinitedusky/indusk-mcp extensions enable dash0
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
If `.indusk/extensions/dash0/.env` has credentials, this auto-configures the MCP server. Otherwise it tells you what to create.
|
|
115
|
+
|
|
116
|
+
### 7. Verify
|
|
117
|
+
|
|
118
|
+
- Start the app, hit an endpoint
|
|
119
|
+
- Check Dash0 for traces: `mcp__dash0__getSpans` with `service.name = my-app`
|
|
120
|
+
- Check health: `mcp__indusk__check_health` should show OTel extension healthy
|
|
121
|
+
|
|
51
122
|
## Before Modifying Code
|
|
52
123
|
|
|
53
124
|
Before touching any file:
|
package/skills/work.md
CHANGED
|
@@ -52,14 +52,15 @@ Implementation plans live in `planning/{plan-name}/impl.md` as checklists. Your
|
|
|
52
52
|
- Add it as a new item in the appropriate phase
|
|
53
53
|
- Then do it and check it off
|
|
54
54
|
|
|
55
|
-
10. **Per-phase completion order.** Each phase has up to
|
|
55
|
+
10. **Per-phase completion order.** Each phase has up to five types of items. Complete them in this order:
|
|
56
56
|
|
|
57
57
|
**Implementation items** → build the thing
|
|
58
|
-
**
|
|
58
|
+
**OTel items** → instrument it (spans, categories, error recording — see OTel skill)
|
|
59
|
+
**Verification items** → prove it works (tests, type checks, commands — can include trace verification)
|
|
59
60
|
**Context items** → capture what changed (concrete CLAUDE.md edits)
|
|
60
61
|
**Document items** → write or update docs pages (see document skill)
|
|
61
62
|
|
|
62
|
-
A phase is not complete until all
|
|
63
|
+
A phase is not complete until all five are done. **Enforced by hooks:** if you try to check off a Phase N+1 implementation item while Phase N has unchecked gates, the edit will be blocked with a message listing what's missing. Complete the gates first.
|
|
63
64
|
|
|
64
65
|
## Gate Override Policy
|
|
65
66
|
|
|
@@ -171,7 +172,7 @@ Then **stop and wait** for the user to say "continue" before moving to the next
|
|
|
171
172
|
|
|
172
173
|
### At gate transitions:
|
|
173
174
|
|
|
174
|
-
When moving between gates (implement → verify → context → document → next phase), explain the transition: what gate you're entering, why it exists, and what it catches. Example: "Code is written. Now
|
|
175
|
+
When moving between gates (implement → otel → verify → context → document → next phase), explain the transition: what gate you're entering, why it exists, and what it catches. Example: "Code is written. Now OTel — instrument the new code paths. Then verify — type check, lint, tests, trace verification. Then context and docs."
|
|
175
176
|
|
|
176
177
|
### Between checklist items:
|
|
177
178
|
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenTelemetry Instrumentation for Next.js
|
|
3
|
+
*
|
|
4
|
+
* This file is automatically loaded by Next.js via the instrumentation hook (13.4+).
|
|
5
|
+
* Place it in the app root (next to next.config.ts).
|
|
6
|
+
*
|
|
7
|
+
* Configuration via environment variables:
|
|
8
|
+
* OTEL_SERVICE_NAME — service name (defaults to "unknown-service")
|
|
9
|
+
* OTEL_EXPORTER_OTLP_ENDPOINT — OTLP backend URL
|
|
10
|
+
* OTEL_EXPORTER_OTLP_HEADERS — auth headers for the backend
|
|
11
|
+
*
|
|
12
|
+
* Install: pnpm add @vercel/otel
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { registerOTel } from "@vercel/otel";
|
|
16
|
+
|
|
17
|
+
export function register() {
|
|
18
|
+
registerOTel({
|
|
19
|
+
serviceName: process.env.OTEL_SERVICE_NAME ?? "unknown-service",
|
|
20
|
+
});
|
|
21
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenTelemetry Browser Instrumentation
|
|
3
|
+
*
|
|
4
|
+
* Captures page loads, HTTP requests (fetch/XHR), and user interactions
|
|
5
|
+
* from the browser and sends them to any OTLP-compatible backend.
|
|
6
|
+
*
|
|
7
|
+
* Import this as early as possible in your app (main.tsx or App.tsx):
|
|
8
|
+
* import './instrumentation';
|
|
9
|
+
*
|
|
10
|
+
* Configuration via environment variables (use VITE_ or NEXT_PUBLIC_ prefix):
|
|
11
|
+
* VITE_OTEL_SERVICE_NAME — service name
|
|
12
|
+
* VITE_OTEL_EXPORTER_OTLP_ENDPOINT — OTLP backend URL
|
|
13
|
+
* VITE_OTEL_EXPORTER_OTLP_HEADERS — auth headers (e.g., "Authorization=Bearer xxx")
|
|
14
|
+
*
|
|
15
|
+
* Install:
|
|
16
|
+
* pnpm add @opentelemetry/sdk-trace-web @opentelemetry/instrumentation-fetch
|
|
17
|
+
* pnpm add @opentelemetry/instrumentation-document-load @opentelemetry/instrumentation-user-interaction
|
|
18
|
+
* pnpm add @opentelemetry/exporter-trace-otlp-http @opentelemetry/resources @opentelemetry/semantic-conventions
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { WebTracerProvider } from "@opentelemetry/sdk-trace-web";
|
|
22
|
+
import { BatchSpanProcessor, SimpleSpanProcessor, ConsoleSpanExporter } from "@opentelemetry/sdk-trace-base";
|
|
23
|
+
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
|
|
24
|
+
import { resourceFromAttributes } from "@opentelemetry/resources";
|
|
25
|
+
import { ATTR_SERVICE_NAME } from "@opentelemetry/semantic-conventions";
|
|
26
|
+
import { FetchInstrumentation } from "@opentelemetry/instrumentation-fetch";
|
|
27
|
+
import { DocumentLoadInstrumentation } from "@opentelemetry/instrumentation-document-load";
|
|
28
|
+
import { UserInteractionInstrumentation } from "@opentelemetry/instrumentation-user-interaction";
|
|
29
|
+
import { registerInstrumentations } from "@opentelemetry/instrumentation";
|
|
30
|
+
|
|
31
|
+
const endpoint =
|
|
32
|
+
import.meta.env?.VITE_OTEL_EXPORTER_OTLP_ENDPOINT
|
|
33
|
+
?? process.env.NEXT_PUBLIC_OTEL_EXPORTER_OTLP_ENDPOINT
|
|
34
|
+
?? "";
|
|
35
|
+
|
|
36
|
+
const headers =
|
|
37
|
+
import.meta.env?.VITE_OTEL_EXPORTER_OTLP_HEADERS
|
|
38
|
+
?? process.env.NEXT_PUBLIC_OTEL_EXPORTER_OTLP_HEADERS
|
|
39
|
+
?? "";
|
|
40
|
+
|
|
41
|
+
const serviceName =
|
|
42
|
+
import.meta.env?.VITE_OTEL_SERVICE_NAME
|
|
43
|
+
?? process.env.NEXT_PUBLIC_OTEL_SERVICE_NAME
|
|
44
|
+
?? "unknown-service";
|
|
45
|
+
|
|
46
|
+
// Parse headers string: "Key1=Value1,Key2=Value2" → { Key1: "Value1", Key2: "Value2" }
|
|
47
|
+
function parseHeaders(headerStr: string): Record<string, string> {
|
|
48
|
+
if (!headerStr) return {};
|
|
49
|
+
const result: Record<string, string> = {};
|
|
50
|
+
for (const pair of headerStr.split(",")) {
|
|
51
|
+
const eqIdx = pair.indexOf("=");
|
|
52
|
+
if (eqIdx > 0) {
|
|
53
|
+
result[pair.slice(0, eqIdx).trim()] = pair.slice(eqIdx + 1).trim();
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return result;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const provider = new WebTracerProvider({
|
|
60
|
+
resource: resourceFromAttributes({
|
|
61
|
+
[ATTR_SERVICE_NAME]: serviceName,
|
|
62
|
+
}),
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
if (endpoint) {
|
|
66
|
+
const exporter = new OTLPTraceExporter({
|
|
67
|
+
url: `${endpoint}/v1/traces`,
|
|
68
|
+
headers: parseHeaders(headers),
|
|
69
|
+
});
|
|
70
|
+
provider.addSpanProcessor(new BatchSpanProcessor(exporter));
|
|
71
|
+
} else {
|
|
72
|
+
provider.addSpanProcessor(new SimpleSpanProcessor(new ConsoleSpanExporter()));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
provider.register();
|
|
76
|
+
|
|
77
|
+
registerInstrumentations({
|
|
78
|
+
instrumentations: [
|
|
79
|
+
new FetchInstrumentation(),
|
|
80
|
+
new DocumentLoadInstrumentation(),
|
|
81
|
+
new UserInteractionInstrumentation(),
|
|
82
|
+
],
|
|
83
|
+
});
|