@reactionary/source 0.0.38 → 0.0.39

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/core/package.json CHANGED
@@ -5,7 +5,6 @@
5
5
  "types": "src/index.d.ts",
6
6
  "dependencies": {
7
7
  "zod": "4.0.0-beta.20250430T185432",
8
- "@upstash/redis": "^1.34.9",
9
- "reflect-metadata": "0.2.2"
8
+ "@upstash/redis": "^1.34.9"
10
9
  }
11
10
  }
package/core/src/index.ts CHANGED
@@ -6,8 +6,6 @@ export * from './cache/noop-cache';
6
6
  export * from './client/client';
7
7
  export * from './client/client-builder';
8
8
 
9
- export * from './decorators/trpc.decorators';
10
-
11
9
  export * from './providers/analytics.provider';
12
10
  export * from './providers/base.provider';
13
11
  export * from './providers/cart.provider';
@@ -0,0 +1,30 @@
1
+ {
2
+ "jsc": {
3
+ "target": "es2017",
4
+ "parser": {
5
+ "syntax": "typescript",
6
+ "decorators": true,
7
+ "dynamicImport": true
8
+ },
9
+ "transform": {
10
+ "decoratorMetadata": true,
11
+ "legacyDecorator": true
12
+ },
13
+ "keepClassNames": true,
14
+ "externalHelpers": true,
15
+ "loose": true
16
+ },
17
+ "module": {
18
+ "type": "commonjs"
19
+ },
20
+ "sourceMaps": true,
21
+ "exclude": [
22
+ "jest.config.ts",
23
+ ".*\\.spec.tsx?$",
24
+ ".*\\.test.tsx?$",
25
+ "./src/jest-setup.ts$",
26
+ "./**/jest-setup.ts$",
27
+ ".*.js$",
28
+ ".*.d.ts$"
29
+ ]
30
+ }
@@ -0,0 +1,21 @@
1
+ import { FlatCompat } from '@eslint/eslintrc';
2
+ import { dirname } from 'path';
3
+ import { fileURLToPath } from 'url';
4
+ import js from '@eslint/js';
5
+ import { fixupConfigRules } from '@eslint/compat';
6
+ import nx from '@nx/eslint-plugin';
7
+ import baseConfig from '../../eslint.config.mjs';
8
+ const compat = new FlatCompat({
9
+ baseDirectory: dirname(fileURLToPath(import.meta.url)),
10
+ recommendedConfig: js.configs.recommended,
11
+ });
12
+
13
+ export default [
14
+ ...fixupConfigRules(compat.extends('next')),
15
+ ...fixupConfigRules(compat.extends('next/core-web-vitals')),
16
+ ...baseConfig,
17
+ ...nx.configs['flat/react-typescript'],
18
+ {
19
+ ignores: ['.next/**/*'],
20
+ },
21
+ ];
@@ -0,0 +1,6 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ declare module '*.svg' {
3
+ const content: any;
4
+ export const ReactComponent: any;
5
+ export default content;
6
+ }
@@ -0,0 +1,5 @@
1
+ /// <reference types="next" />
2
+ /// <reference types="next/image-types/global" />
3
+
4
+ // NOTE: This file should not be edited
5
+ // see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
@@ -0,0 +1,20 @@
1
+ //@ts-check
2
+
3
+
4
+ const { composePlugins, withNx } = require('@nx/next');
5
+
6
+ /**
7
+ * @type {import('@nx/next/plugins/with-nx').WithNxOptions}
8
+ **/
9
+ const nextConfig = {
10
+ // Use this to set Nx-specific options
11
+ // See: https://nx.dev/recipes/next/next-config-setup
12
+ nx: {},
13
+ };
14
+
15
+ const plugins = [
16
+ // Add more Next.js plugins to this list if needed.
17
+ withNx,
18
+ ];
19
+
20
+ module.exports = composePlugins(...plugins)(nextConfig);
@@ -0,0 +1,9 @@
1
+ {
2
+ "name": "next",
3
+ "$schema": "../../node_modules/nx/schemas/project-schema.json",
4
+ "sourceRoot": "examples/next",
5
+ "projectType": "application",
6
+ "tags": [],
7
+ "// targets": "to see all targets run: nx show project next --web",
8
+ "targets": {}
9
+ }
File without changes
Binary file
File without changes
@@ -0,0 +1,18 @@
1
+ import './global.css';
2
+
3
+ export const metadata = {
4
+ title: 'Welcome to next',
5
+ description: 'Generated by create-nx-workspace',
6
+ };
7
+
8
+ export default function RootLayout({
9
+ children,
10
+ }: {
11
+ children: React.ReactNode;
12
+ }) {
13
+ return (
14
+ <html lang="en">
15
+ <body>{children}</body>
16
+ </html>
17
+ );
18
+ }
@@ -0,0 +1,2 @@
1
+ .page {
2
+ }
@@ -0,0 +1,51 @@
1
+ import styles from './page.module.scss';
2
+ import { ClientBuilder, NoOpCache, SessionSchema } from '@reactionary/core';
3
+ import { withFakeCapabilities } from '@reactionary/provider-fake';
4
+
5
+ export default async function Index() {
6
+ const client = new ClientBuilder()
7
+ .withCapability(
8
+ withFakeCapabilities(
9
+ {
10
+ jitter: {
11
+ mean: 0,
12
+ deviation: 0,
13
+ },
14
+ seeds: {
15
+ product: 1,
16
+ search: 1,
17
+ category: 1,
18
+ },
19
+ },
20
+ { search: true, product: false, identity: false }
21
+ )
22
+ )
23
+ .withCache(new NoOpCache())
24
+ .build();
25
+
26
+ const session = SessionSchema.parse({
27
+ id: '1234567890',
28
+ languageContext: {
29
+ countryCode: 'US',
30
+ languageCode: 'en',
31
+ currencyCode: 'USD',
32
+ },
33
+ });
34
+
35
+ const search = await client.search?.queryByTerm({
36
+ search: {
37
+ facets: [],
38
+ page: 0,
39
+ pageSize: 12,
40
+ term: 'glass',
41
+ },
42
+ }, session);
43
+
44
+ return <div className={styles.page}>
45
+ {search?.products.map((product, index) => (
46
+ <div key={index}>
47
+ { product.name }
48
+ </div>
49
+ ))}
50
+ </div>;
51
+ }
@@ -0,0 +1,9 @@
1
+ export async function register() {
2
+ if (process.env.NEXT_RUNTIME === 'nodejs') {
3
+ const { NodeSDK } = await import('@opentelemetry/sdk-node');
4
+
5
+ const sdk = new NodeSDK();
6
+
7
+ sdk.start();
8
+ }
9
+ }
@@ -0,0 +1,44 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "jsx": "preserve",
5
+ "strict": true,
6
+ "noEmit": true,
7
+ "emitDeclarationOnly": false,
8
+ "esModuleInterop": true,
9
+ "module": "esnext",
10
+ "moduleResolution": "bundler",
11
+ "resolveJsonModule": true,
12
+ "isolatedModules": true,
13
+ "lib": [
14
+ "dom",
15
+ "dom.iterable",
16
+ "esnext"
17
+ ],
18
+ "allowJs": true,
19
+ "allowSyntheticDefaultImports": true,
20
+ "forceConsistentCasingInFileNames": true,
21
+ "incremental": true,
22
+ "plugins": [
23
+ {
24
+ "name": "next"
25
+ }
26
+ ]
27
+ },
28
+ "include": [
29
+ "**/*.js",
30
+ "**/*.jsx",
31
+ "**/*.ts",
32
+ "**/*.tsx",
33
+ "../../dist/examples/next/.next/types/**/*.ts",
34
+ "../../examples/next/.next/types/**/*.ts",
35
+ "next-env.d.ts",
36
+ ".next/types/**/*.ts"
37
+ ],
38
+ "exclude": [
39
+ "node_modules",
40
+ "jest.config.ts",
41
+ "**/*.spec.ts",
42
+ "**/*.test.ts"
43
+ ]
44
+ }
@@ -68,7 +68,6 @@ describe('basic node provider extension (models)', () => {
68
68
  it('should get the enabled set of capabilities across providers', async () => {
69
69
  expect(client.product).toBeDefined();
70
70
  expect(client.search).toBeDefined();
71
- expect(client.identity).toBeUndefined();
72
71
  });
73
72
 
74
73
  it('should be able to call the regular methods and get the default value', async () => {
@@ -26,7 +26,6 @@ describe('basic node setup', () => {
26
26
  it('should only get back the enabled capabilities', async () => {
27
27
  expect(client.product).toBeDefined();
28
28
  expect(client.search).toBeDefined();
29
- expect(client.identity).toBeUndefined();
30
29
  });
31
30
 
32
31
  it('should be able to call the enabled capabilities', async () => {
package/otel/README.md CHANGED
@@ -1,16 +1,18 @@
1
1
  # @reactionary/otel
2
2
 
3
- Zero-configuration OpenTelemetry instrumentation for Reactionary framework. Automatically instruments tRPC routes and providers using standard OTEL environment variables.
3
+ OpenTelemetry instrumentation for the Reactionary framework. Provides decorators and utilities for tracing function execution and performance monitoring.
4
+
5
+ ## Important: SDK Initialization Required
6
+
7
+ This library provides **instrumentation only**. The host application is responsible for initializing the OpenTelemetry SDK. Without proper SDK initialization, traces will be created as `NonRecordingSpan` instances with zero trace/span IDs.
4
8
 
5
9
  ## Features
6
10
 
7
- - **Zero Configuration**: Auto-initializes on first use with standard OTEL env vars
8
- - **Automatic tRPC Route Tracing**: All tRPC procedures automatically traced
9
- - **Provider Instrumentation**: BaseProvider operations automatically instrumented
10
- - **Standard Compliance**: Uses official OpenTelemetry environment variables
11
- - **Multiple Exporters**: Console, OTLP/HTTP, or custom exporters
12
- - **Metrics Collection**: Request counts, durations, errors automatically tracked
13
- - **Lazy Initialization**: Only starts when actually used
11
+ - **Tracing Decorators**: Automatic span creation for decorated methods
12
+ - **Manual Instrumentation**: Utilities for custom tracing
13
+ - **Framework Integration**: Built-in support for tRPC and providers
14
+ - **Zero Dependencies**: Only requires OpenTelemetry API
15
+ - **Graceful Degradation**: Works without SDK initialization (produces no-op spans)
14
16
 
15
17
  ## Installation
16
18
 
@@ -18,230 +20,208 @@ Zero-configuration OpenTelemetry instrumentation for Reactionary framework. Auto
18
20
  pnpm add @reactionary/otel
19
21
  ```
20
22
 
21
- That's it! No initialization code needed.
23
+ **Important**: You must also install and configure the OpenTelemetry SDK in your host application.
22
24
 
23
- ## How It Works
25
+ ## Usage
24
26
 
25
- The OTEL package automatically initializes itself on first use, reading configuration from standard OpenTelemetry environment variables. When your code first creates a span or metric, the SDK initializes with your environment configuration.
27
+ ### Basic Tracing with Decorators
26
28
 
27
29
  ```typescript
28
- // No imports or initialization needed!
29
- // Just use your tRPC router or providers normally
30
- import { createTRPCRouter } from '@reactionary/trpc';
31
-
32
- const router = createTRPCRouter(client);
33
- // ↑ Automatically instrumented when OTEL env vars are set
34
- ```
35
-
36
- ## Configuration
37
-
38
- Use standard OpenTelemetry environment variables. No code changes needed.
39
-
40
- ### Standard Environment Variables
41
-
42
- ```bash
43
- # Service identification
44
- OTEL_SERVICE_NAME=my-service
45
- OTEL_SERVICE_VERSION=1.0.0
46
-
47
- # Traces exporter (console | otlp | otlp/http | none)
48
- OTEL_TRACES_EXPORTER=otlp
49
-
50
- # Metrics exporter (console | otlp | otlp/http | none)
51
- OTEL_METRICS_EXPORTER=otlp
52
-
53
- # OTLP endpoint and headers
54
- OTEL_EXPORTER_OTLP_ENDPOINT=https://api.honeycomb.io
55
- OTEL_EXPORTER_OTLP_HEADERS=x-honeycomb-team=your-api-key
56
-
57
- # Or use specific endpoints
58
- OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=https://api.honeycomb.io/v1/traces
59
- OTEL_EXPORTER_OTLP_METRICS_ENDPOINT=https://api.honeycomb.io/v1/metrics
60
-
61
- # Debug logging
62
- OTEL_LOG_LEVEL=debug
63
-
64
- # Metrics export interval (milliseconds)
65
- OTEL_METRIC_EXPORT_INTERVAL=60000
66
- ```
30
+ import { traced } from '@reactionary/otel';
67
31
 
68
- See the [OpenTelemetry specification](https://opentelemetry.io/docs/specs/otel/configuration/sdk-environment-variables/) for all available options.
69
-
70
- ## Exporters
71
-
72
- ### Console (Development)
73
- ```bash
74
- OTEL_TRACES_EXPORTER=console
75
- OTEL_METRICS_EXPORTER=console
76
- ```
77
-
78
- ### OTLP (Production)
79
- Works with any OTLP-compatible backend:
80
-
81
- ```bash
82
- OTEL_TRACES_EXPORTER=otlp
83
- OTEL_METRICS_EXPORTER=otlp
84
- OTEL_EXPORTER_OTLP_ENDPOINT=https://api.honeycomb.io
85
- OTEL_EXPORTER_OTLP_HEADERS=x-honeycomb-team=your-api-key
86
- ```
32
+ class MyService {
33
+ @traced()
34
+ async fetchData(id: string): Promise<Data> {
35
+ // This method will be automatically traced
36
+ return await dataSource.get(id);
37
+ }
87
38
 
88
- ### Disable
89
- ```bash
90
- OTEL_TRACES_EXPORTER=none
91
- OTEL_METRICS_EXPORTER=none
39
+ @traced({
40
+ spanName: 'custom-operation',
41
+ captureResult: false
42
+ })
43
+ processData(data: Data): void {
44
+ // Custom span name and no result capture
45
+ }
46
+ }
92
47
  ```
93
48
 
94
- ## Custom Instrumentation
95
-
96
- ### Manual Spans
97
-
98
- Create custom spans for specific operations:
49
+ ### Manual Instrumentation
99
50
 
100
51
  ```typescript
101
52
  import { withSpan, getTracer } from '@reactionary/otel';
102
53
 
103
54
  // Using withSpan helper
104
- const result = await withSpan('custom-operation', async (span) => {
105
- span.setAttribute('custom.attribute', 'value');
106
- // Your operation here
107
- return someAsyncOperation();
55
+ const result = await withSpan('my-operation', async (span) => {
56
+ span.setAttribute('operation.id', operationId);
57
+ return await performOperation();
108
58
  });
109
59
 
110
60
  // Using tracer directly
111
61
  const tracer = getTracer();
112
62
  const span = tracer.startSpan('manual-span');
113
63
  try {
114
- // Your operation
64
+ // Your code here
115
65
  span.setStatus({ code: SpanStatusCode.OK });
116
66
  } catch (error) {
117
- span.recordException(error);
118
67
  span.setStatus({ code: SpanStatusCode.ERROR });
119
- throw error;
68
+ span.recordException(error);
120
69
  } finally {
121
70
  span.end();
122
71
  }
123
72
  ```
124
73
 
125
- ### Provider Instrumentation
74
+ ## Setting Up OpenTelemetry SDK
126
75
 
127
- Providers are automatically instrumented when extending BaseProvider:
76
+ ### Next.js Applications
128
77
 
129
- ```typescript
130
- import { BaseProvider } from '@reactionary/core';
78
+ 1. Create an `instrumentation.ts` file in your project root:
131
79
 
132
- class MyProvider extends BaseProvider {
133
- // Automatically traced when OTEL is initialized
134
- protected async fetch(queries, session) {
135
- // Your implementation
136
- }
137
-
138
- protected async process(mutations, session) {
139
- // Your implementation
80
+ ```typescript
81
+ // instrumentation.ts
82
+ export async function register() {
83
+ if (process.env.NEXT_RUNTIME === 'nodejs') {
84
+ const { NodeSDK } = await import('@opentelemetry/sdk-node');
85
+ const { getNodeAutoInstrumentations } = await import('@opentelemetry/auto-instrumentations-node');
86
+
87
+ const sdk = new NodeSDK({
88
+ instrumentations: [getNodeAutoInstrumentations()],
89
+ });
90
+
91
+ sdk.start();
140
92
  }
141
93
  }
142
94
  ```
143
95
 
144
- ### Custom Metrics
96
+ 2. Enable instrumentation in `next.config.js`:
145
97
 
146
- Track custom business metrics:
98
+ ```javascript
99
+ /** @type {import('next').NextConfig} */
100
+ const nextConfig = {
101
+ experimental: {
102
+ instrumentationHook: true,
103
+ },
104
+ };
147
105
 
148
- ```typescript
149
- import { getMetrics } from '@reactionary/otel';
106
+ module.exports = nextConfig;
107
+ ```
150
108
 
151
- const metrics = getMetrics();
109
+ 3. Configure environment variables:
152
110
 
153
- // Increment counter
154
- metrics.requestCounter.add(1, {
155
- 'endpoint': '/api/users',
156
- 'method': 'GET'
157
- });
111
+ ```bash
112
+ # .env.local
113
+ OTEL_SERVICE_NAME=my-nextjs-app
114
+ OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318
115
+ OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf
116
+ ```
117
+
118
+ ### Node.js Applications
158
119
 
159
- // Record histogram
160
- metrics.requestDuration.record(150, {
161
- 'endpoint': '/api/users',
162
- 'status': 'success'
120
+ ```typescript
121
+ // Initialize at the very beginning of your application
122
+ import { NodeSDK } from '@opentelemetry/sdk-node';
123
+ import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';
124
+
125
+ const sdk = new NodeSDK({
126
+ instrumentations: [getNodeAutoInstrumentations()],
163
127
  });
128
+
129
+ sdk.start();
130
+
131
+ // Now import and use your application code
132
+ import './app';
164
133
  ```
165
134
 
166
- ## Examples
135
+ ### Environment Variables
167
136
 
168
- ### Honeycomb
137
+ The OpenTelemetry SDK can be configured using environment variables:
169
138
 
170
139
  ```bash
171
- OTEL_SERVICE_NAME=my-service
172
- OTEL_TRACES_EXPORTER=otlp
173
- OTEL_EXPORTER_OTLP_ENDPOINT=https://api.honeycomb.io
174
- OTEL_EXPORTER_OTLP_HEADERS=x-honeycomb-team=your-api-key
175
- ```
140
+ # Service identification
141
+ OTEL_SERVICE_NAME=my-app
142
+ OTEL_SERVICE_VERSION=1.0.0
176
143
 
177
- ### Local Development
144
+ # OTLP Exporter (for Jaeger, etc.)
145
+ OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318
146
+ OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf
178
147
 
179
- ```bash
180
- OTEL_SERVICE_NAME=my-service-dev
148
+ # Console exporter (for development)
181
149
  OTEL_TRACES_EXPORTER=console
182
- OTEL_METRICS_EXPORTER=none # Disable metrics in dev
150
+
151
+ # Sampling (optional)
152
+ OTEL_TRACES_SAMPLER=always_on
153
+
154
+ # Debug logging
155
+ OTEL_LOG_LEVEL=debug
183
156
  ```
184
157
 
185
- ### Docker Compose with Jaeger
186
-
187
- ```yaml
188
- services:
189
- app:
190
- environment:
191
- - OTEL_EXPORTER_TYPE=otlp
192
- - OTEL_COLLECTOR_ENDPOINT=http://jaeger:4318
193
- - OTEL_SERVICE_NAME=my-service
194
-
195
- jaeger:
196
- image: jaegertracing/all-in-one:latest
197
- ports:
198
- - "16686:16686" # Jaeger UI
199
- - "4318:4318" # OTLP HTTP
158
+ ## Troubleshooting
159
+
160
+ ### ProxyTracer with NonRecordingSpan
161
+
162
+ If you see logs like:
163
+ ```
164
+ tracer: ProxyTracer { _provider: ProxyTracerProvider {} }
165
+ ending span: NonRecordingSpan { _spanContext: { traceId: '00000000000000000000000000000000' } }
200
166
  ```
201
167
 
202
- ## Metrics Reference
168
+ This means the OpenTelemetry SDK has not been initialized. Ensure you have:
169
+ 1. Created an `instrumentation.ts` file (Next.js)
170
+ 2. Initialized the SDK at application startup (Node.js)
171
+ 3. Set the required environment variables
203
172
 
204
- The following metrics are automatically collected:
173
+ ### Missing Traces
205
174
 
206
- | Metric | Type | Description |
207
- |--------|------|-------------|
208
- | `reactionary.requests` | Counter | Total number of requests |
209
- | `reactionary.request.duration` | Histogram | Request duration in milliseconds |
210
- | `reactionary.requests.active` | UpDownCounter | Number of active requests |
211
- | `reactionary.errors` | Counter | Total number of errors |
212
- | `reactionary.provider.calls` | Counter | Total provider calls |
213
- | `reactionary.provider.duration` | Histogram | Provider call duration |
214
- | `reactionary.cache.hits` | Counter | Cache hit count |
215
- | `reactionary.cache.misses` | Counter | Cache miss count |
175
+ If the decorator is being applied but you don't see traces:
176
+ 1. Verify the OTEL exporter is configured correctly
177
+ 2. Check that your tracing backend is running
178
+ 3. Ensure sampling is enabled (`OTEL_TRACES_SAMPLER=always_on`)
216
179
 
217
- ## Best Practices
180
+ ## API Reference
218
181
 
219
- 1. **Use Standard Variables**: Stick to OpenTelemetry standard environment variables
220
- 2. **Set Service Name**: Always set `OTEL_SERVICE_NAME` for service identification
221
- 3. **Environment-based Config**: Use different configs for dev/staging/production
222
- 4. **Add Context**: Use span attributes to add business context to traces
223
- 5. **Handle Errors**: Ensure spans are properly closed even on errors
224
- 6. **Sample Wisely**: Consider sampling strategies for high-volume services
225
- 7. **Monitor Performance**: Watch for overhead in high-throughput scenarios
182
+ ### Decorators
226
183
 
227
- ## Troubleshooting
184
+ #### `@traced(options?)`
228
185
 
229
- ### Traces Not Appearing
186
+ Decorates a method to automatically create spans for its execution.
230
187
 
231
- 1. Check OTEL is initialized before other components
232
- 2. Verify `OTEL_TRACE_ENABLED` is not set to `false`
233
- 3. Check exporter configuration and endpoint connectivity
234
- 4. Look for initialization errors in console
188
+ **Options:**
189
+ - `captureArgs?: boolean` - Capture function arguments (default: true)
190
+ - `captureResult?: boolean` - Capture return value (default: true)
191
+ - `spanName?: string` - Custom span name (default: ClassName.methodName)
192
+ - `spanKind?: SpanKind` - OpenTelemetry span kind (default: INTERNAL)
235
193
 
236
- ### Performance Impact
194
+ ### Utility Functions
195
+
196
+ - `getTracer(): Tracer` - Get the library's tracer instance
197
+ - `startSpan(name, options?, context?): Span` - Start a new span
198
+ - `withSpan<T>(name, fn, options?): Promise<T>` - Execute function within a span
199
+ - `setSpanAttributes(span, attributes): void` - Set multiple span attributes
200
+ - `createChildSpan(parent, name, options?): Span` - Create child span
201
+
202
+ ### Constants
203
+
204
+ - `SpanKind` - OpenTelemetry span kinds
205
+ - `SpanStatusCode` - OpenTelemetry span status codes
206
+
207
+ ## Examples
237
208
 
238
- - Use sampling to reduce overhead
239
- - Disable metrics if not needed
240
- - Consider using batch exporters
241
- - Increase export intervals for metrics
209
+ ### Console Output (Development)
210
+ ```bash
211
+ OTEL_SERVICE_NAME=my-service-dev
212
+ OTEL_TRACES_EXPORTER=console
213
+ ```
242
214
 
243
- ### Memory Usage
215
+ ### Jaeger (Local)
216
+ ```bash
217
+ OTEL_SERVICE_NAME=my-service
218
+ OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318
219
+ OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf
220
+ ```
244
221
 
245
- - Monitor span processor queue size
246
- - Adjust batch size and timeout
247
- - Consider using sampling for high-volume services
222
+ ### Honeycomb (Production)
223
+ ```bash
224
+ OTEL_SERVICE_NAME=my-service
225
+ OTEL_EXPORTER_OTLP_ENDPOINT=https://api.honeycomb.io
226
+ OTEL_EXPORTER_OTLP_HEADERS=x-honeycomb-team=your-api-key
227
+ ```
package/otel/package.json CHANGED
@@ -6,7 +6,6 @@
6
6
  "types": "./src/index.d.ts",
7
7
  "dependencies": {
8
8
  "@opentelemetry/api": "^1.9.0",
9
- "@opentelemetry/sdk-node": "^0.203.0",
10
9
  "@trpc/server": "^11.1.2"
11
10
  }
12
11
  }
package/otel/src/index.ts CHANGED
@@ -1,12 +1,22 @@
1
- // OpenTelemetry auto-initialization based on standard environment variables
2
- // See: https://opentelemetry.io/docs/specs/otel/configuration/sdk-environment-variables/
1
+ // OpenTelemetry instrumentation library
2
+ // This library provides instrumentation only - the host application
3
+ // is responsible for initializing the OpenTelemetry SDK
3
4
 
4
5
  // Framework integration exports (internal use only)
5
6
  export { createTRPCTracing } from './trpc-middleware';
6
7
  export { createProviderInstrumentation } from './provider-instrumentation';
7
8
 
8
9
  // Decorator for tracing functions
9
- export { traced, TracedOptions } from './trace-decorator';
10
- export { getTracer } from './tracer';
11
- // Graceful shutdown for process termination
12
- export { shutdownOtel } from './sdk';
10
+ export { traced } from './trace-decorator';
11
+ export type { TracedOptions } from './trace-decorator';
12
+
13
+ // Utility functions for manual instrumentation
14
+ export {
15
+ getTracer,
16
+ startSpan,
17
+ withSpan,
18
+ setSpanAttributes,
19
+ createChildSpan,
20
+ SpanKind,
21
+ SpanStatusCode
22
+ } from './tracer';
@@ -1,5 +1,4 @@
1
1
  import { metrics, Meter, Counter, Histogram, UpDownCounter } from '@opentelemetry/api';
2
- import { isOtelInitialized } from './sdk';
3
2
 
4
3
  const METER_NAME = '@reactionary/otel';
5
4
  const METER_VERSION = '0.0.1';
@@ -8,8 +7,9 @@ let globalMeter: Meter | null = null;
8
7
 
9
8
  export function getMeter(): Meter {
10
9
  if (!globalMeter) {
11
- // Ensure OTEL is initialized before creating meter
12
- isOtelInitialized();
10
+ // Simply get the meter from the API
11
+ // If the SDK is not initialized by the host application,
12
+ // this will return a NoopMeter
13
13
  globalMeter = metrics.getMeter(METER_NAME, METER_VERSION);
14
14
  }
15
15
  return globalMeter;
@@ -84,8 +84,8 @@ function safeSerialize(value: unknown, maxDepth = 3, currentDepth = 0): string {
84
84
  /**
85
85
  * TypeScript decorator for tracing function execution
86
86
  * Automatically creates OpenTelemetry spans for decorated methods
87
- * Supports both Stage 2 (legacy) and Stage 3 decorator syntax
88
- *
87
+ * Uses Stage 2 (legacy) decorator syntax
88
+ *
89
89
  * @example
90
90
  * ```typescript
91
91
  * class MyService {
@@ -101,7 +101,7 @@ function safeSerialize(value: unknown, maxDepth = 3, currentDepth = 0): string {
101
101
  * }
102
102
  * ```
103
103
  */
104
- export function traced(options: TracedOptions = {}): any {
104
+ export function traced(options: TracedOptions = {}): MethodDecorator {
105
105
  const {
106
106
  captureArgs = false,
107
107
  captureResult = false,
@@ -109,42 +109,22 @@ export function traced(options: TracedOptions = {}): any {
109
109
  spanKind = SpanKind.INTERNAL
110
110
  } = options;
111
111
 
112
- // Stage 2 (legacy) decorator
113
112
  return function (
114
113
  target: any,
115
- propertyKey?: string | symbol,
116
- descriptor?: PropertyDescriptor
117
- ): any {
118
- // Handle Stage 3 decorator (when called with context)
119
- if (typeof propertyKey === 'object' && propertyKey && 'kind' in propertyKey) {
120
- const context = propertyKey as any;
121
- const originalMethod = target;
122
- const methodName = String(context.name);
123
-
124
- return createTracedMethod(originalMethod, methodName, {
125
- captureArgs,
126
- captureResult,
127
- spanName,
128
- spanKind
129
- });
130
- }
131
-
132
- // Handle Stage 2 decorator
133
- if (descriptor && typeof descriptor.value === 'function') {
134
- const originalMethod = descriptor.value;
135
- const methodName = String(propertyKey);
136
-
137
- descriptor.value = createTracedMethod(originalMethod, methodName, {
138
- captureArgs,
139
- captureResult,
140
- spanName,
141
- spanKind
142
- });
143
-
144
- return descriptor;
145
- }
146
-
147
- return target;
114
+ propertyKey: string | symbol,
115
+ descriptor: PropertyDescriptor
116
+ ): PropertyDescriptor {
117
+ const originalMethod = descriptor.value;
118
+ const methodName = String(propertyKey);
119
+
120
+ descriptor.value = createTracedMethod(originalMethod, methodName, {
121
+ captureArgs,
122
+ captureResult,
123
+ spanName,
124
+ spanKind
125
+ });
126
+
127
+ return descriptor;
148
128
  };
149
129
  }
150
130
 
@@ -159,81 +139,80 @@ function createTracedMethod(
159
139
  }
160
140
  ): any {
161
141
  const { captureArgs, captureResult, spanName, spanKind } = options;
162
-
163
- function tracedMethod(this: any, ...args: any[]): any {
142
+
143
+ async function tracedMethod(this: any, ...args: any[]): Promise<any> {
164
144
  const tracer = getTracer();
165
145
  const className = this?.constructor?.name || 'Unknown';
166
146
  const effectiveSpanName = spanName || `${className}.${methodName}`;
167
147
 
168
- // Start the span
169
- const span = tracer.startSpan(effectiveSpanName, {
148
+ // Use startActiveSpan to ensure proper context propagation
149
+ return tracer.startActiveSpan(effectiveSpanName, {
170
150
  kind: spanKind,
171
151
  attributes: {
172
152
  'function.name': methodName,
173
153
  'function.class': className,
174
154
  }
175
- });
176
-
177
- // Capture arguments if enabled
178
- if (captureArgs && args.length > 0) {
179
- args.forEach((arg, index) => {
180
- try {
181
- span.setAttribute(`function.args.${index}`, safeSerialize(arg));
182
- } catch {
183
- span.setAttribute(`function.args.${index}`, '[Serialization error]');
184
- }
185
- });
186
- span.setAttribute('function.args.count', args.length);
187
- }
188
-
189
- // Helper function to finalize span with result
190
- const finalizeSpan = (result: unknown, isError = false) => {
191
- if (!isError && captureResult && result !== undefined) {
192
- try {
193
- span.setAttribute('function.result', safeSerialize(result));
194
- } catch {
195
- span.setAttribute('function.result', '[Serialization error]');
196
- }
197
- }
198
-
199
- if (isError) {
200
- span.setStatus({
201
- code: SpanStatusCode.ERROR,
202
- message: result instanceof Error ? result.message : String(result)
155
+ }, async (span) => {
156
+ // Capture arguments if enabled
157
+ if (captureArgs && args.length > 0) {
158
+ args.forEach((arg, index) => {
159
+ try {
160
+ span.setAttribute(`function.args.${index}`, safeSerialize(arg));
161
+ } catch {
162
+ span.setAttribute(`function.args.${index}`, '[Serialization error]');
163
+ }
203
164
  });
204
- if (result instanceof Error) {
205
- span.recordException(result);
206
- }
207
- } else {
208
- span.setStatus({ code: SpanStatusCode.OK });
165
+ span.setAttribute('function.args.count', args.length);
209
166
  }
210
167
 
211
- span.end();
212
- };
213
-
214
- try {
215
- const result = originalMethod.apply(this, args);
216
-
217
- // Handle async functions
218
- if (result instanceof Promise) {
219
- return result
220
- .then((value) => {
221
- finalizeSpan(value);
168
+ // Helper function to set span attributes and status
169
+ const setSpanResult = (result: unknown, isError = false) => {
170
+ if (!isError && captureResult && result !== undefined) {
171
+ try {
172
+ span.setAttribute('function.result', safeSerialize(result));
173
+ } catch {
174
+ span.setAttribute('function.result', '[Serialization error]');
175
+ }
176
+ }
177
+
178
+ if (isError) {
179
+ span.setStatus({
180
+ code: SpanStatusCode.ERROR,
181
+ message: result instanceof Error ? result.message : String(result)
182
+ });
183
+ if (result instanceof Error) {
184
+ span.recordException(result);
185
+ }
186
+ } else {
187
+ span.setStatus({ code: SpanStatusCode.OK });
188
+ }
189
+
190
+ span.end();
191
+ };
192
+
193
+ try {
194
+ const result = originalMethod.apply(this, args);
195
+
196
+ // Handle async functions - await them to keep span open
197
+ if (result instanceof Promise) {
198
+ try {
199
+ const value = await result;
200
+ setSpanResult(value);
222
201
  return value;
223
- })
224
- .catch((error) => {
225
- finalizeSpan(error, true);
202
+ } catch (error) {
203
+ setSpanResult(error, true);
226
204
  throw error;
227
- });
205
+ }
206
+ }
207
+
208
+ // Handle sync functions
209
+ setSpanResult(result);
210
+ return result;
211
+ } catch (error) {
212
+ setSpanResult(error, true);
213
+ throw error;
228
214
  }
229
-
230
- // Handle sync functions
231
- finalizeSpan(result);
232
- return result;
233
- } catch (error) {
234
- finalizeSpan(error, true);
235
- throw error;
236
- }
215
+ });
237
216
  }
238
217
 
239
218
  // Preserve the original function's name and properties
@@ -8,7 +8,6 @@ import {
8
8
  SpanOptions,
9
9
  Attributes,
10
10
  } from '@opentelemetry/api';
11
- import { isOtelInitialized } from './sdk';
12
11
 
13
12
  const TRACER_NAME = '@reactionary/otel';
14
13
  const TRACER_VERSION = '0.0.1';
@@ -17,8 +16,9 @@ let globalTracer: Tracer | null = null;
17
16
 
18
17
  export function getTracer(): Tracer {
19
18
  if (!globalTracer) {
20
- // Ensure OTEL is initialized before creating tracer
21
- isOtelInitialized();
19
+ // Simply get the tracer from the API
20
+ // If the SDK is not initialized by the host application,
21
+ // this will return a ProxyTracer that produces NonRecordingSpans
22
22
  globalTracer = trace.getTracer(TRACER_NAME, TRACER_VERSION);
23
23
  }
24
24
  return globalTracer;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reactionary/source",
3
- "version": "0.0.38",
3
+ "version": "0.0.39",
4
4
  "license": "MIT",
5
5
  "private": false,
6
6
  "dependencies": {
@@ -10,7 +10,7 @@ import {
10
10
  } from '@reactionary/core';
11
11
  import z from 'zod';
12
12
  import { FakeConfiguration } from '../schema/configuration.schema';
13
- import { Faker, en, base } from '@faker-js/faker/.';
13
+ import { Faker, en, base } from '@faker-js/faker';
14
14
 
15
15
  export class FakeCartProvider<
16
16
  T extends Cart = Cart
@@ -1,16 +1,16 @@
1
1
  import {
2
2
  SearchProvider,
3
- SearchQueryByTerm,
4
3
  SearchResult,
5
4
  SearchResultFacet,
6
5
  SearchResultProduct,
7
- Session,
8
6
  Cache as ReactionaryCache,
9
7
  } from '@reactionary/core';
8
+ import type { SearchQueryByTerm, Session } from '@reactionary/core';
10
9
  import z from 'zod';
11
10
  import { FakeConfiguration } from '../schema/configuration.schema';
12
11
  import { Faker, en, base } from '@faker-js/faker';
13
12
  import { jitter } from '../utilities/jitter';
13
+ import { traced } from '@reactionary/otel';
14
14
 
15
15
  export class FakeSearchProvider<
16
16
  T extends SearchResult = SearchResult
@@ -23,6 +23,7 @@ export class FakeSearchProvider<
23
23
  this.config = config;
24
24
  }
25
25
 
26
+ @traced()
26
27
  public override async queryByTerm(
27
28
  payload: SearchQueryByTerm,
28
29
  _session: Session
@@ -119,6 +120,14 @@ export class FakeSearchProvider<
119
120
  },
120
121
  } satisfies SearchResult;
121
122
 
123
+ const foo = this.childFunction();
124
+
122
125
  return this.schema.parse(result);
123
126
  }
127
+
128
+ @traced()
129
+ protected childFunction() {
130
+ const foo = 42;
131
+ return foo;
132
+ }
124
133
  }
package/trpc/package.json CHANGED
@@ -9,7 +9,6 @@
9
9
  "@reactionary/core": "0.0.1",
10
10
  "@reactionary/otel": "0.0.1",
11
11
  "superjson": "^2.2.2",
12
- "zod": "4.0.0-beta.20250430T185432",
13
- "@trpc/client": "^11.1.2"
12
+ "zod": "4.0.0-beta.20250430T185432"
14
13
  }
15
14
  }
@@ -1,7 +1,5 @@
1
- import { Client, Session, isTRPCQuery, isTRPCMutation, isTRPCMethod } from '@reactionary/core';
1
+ import { Client, Session } from '@reactionary/core';
2
2
  import type { TransparentClient } from './types';
3
- import type { inferRouterInputs, inferRouterOutputs } from '@trpc/server';
4
- import type { TRPCClientError } from '@trpc/client';
5
3
 
6
4
  /**
7
5
  * Configuration options for TRPC client creation
@@ -5,6 +5,8 @@
5
5
  "sourceMap": true,
6
6
  "declaration": false,
7
7
  "moduleResolution": "node",
8
+ "emitDecoratorMetadata": true,
9
+ "experimentalDecorators": true,
8
10
  "importHelpers": true,
9
11
  "target": "es2015",
10
12
  "module": "esnext",
@@ -1,144 +0,0 @@
1
- import 'reflect-metadata';
2
-
3
- /**
4
- * Metadata keys for TRPC method decorators
5
- */
6
- export const TRPC_QUERY_METADATA_KEY = Symbol('trpc:query');
7
- export const TRPC_MUTATION_METADATA_KEY = Symbol('trpc:mutation');
8
-
9
-
10
-
11
- /**
12
- * Options for TRPC method decorators
13
- */
14
- export interface TRPCMethodOptions {
15
- /** Custom name for the TRPC procedure (defaults to method name) */
16
- name?: string;
17
- /** Description for documentation purposes */
18
- description?: string;
19
- }
20
-
21
- /**
22
- * Decorator to mark a provider method as a TRPC query procedure
23
- * Query procedures are read-only operations (GET-like)
24
- *
25
- * @example
26
- * ```typescript
27
- * class ProductProvider extends BaseProvider {
28
- * @trpcQuery()
29
- * async getById(payload: ProductQueryById, session: Session): Promise<Product> {
30
- * // implementation
31
- * }
32
- *
33
- * @trpcQuery({ name: 'findBySlug', description: 'Find product by URL slug' })
34
- * async getBySlug(payload: ProductQueryBySlug, session: Session): Promise<Product> {
35
- * // implementation
36
- * }
37
- * }
38
- * ```
39
- */
40
- export function trpcQuery(options: TRPCMethodOptions = {}): MethodDecorator {
41
- return function (target: any, propertyKey: string | symbol, descriptor: PropertyDescriptor) {
42
- // Store metadata about this method being a TRPC query
43
- const metadata = {
44
- methodName: String(propertyKey),
45
- isQuery: true,
46
- isMutation: false,
47
- options
48
- };
49
-
50
- // Store on the prototype so it's inherited
51
- Reflect.defineMetadata(TRPC_QUERY_METADATA_KEY, metadata, target, propertyKey);
52
-
53
- // Also store a list of all TRPC methods on the class
54
- const existingMethods = Reflect.getMetadata(TRPC_QUERY_METADATA_KEY, target.constructor) || [];
55
- existingMethods.push({ propertyKey, metadata });
56
- Reflect.defineMetadata(TRPC_QUERY_METADATA_KEY, existingMethods, target.constructor);
57
- };
58
- }
59
-
60
- /**
61
- * Decorator to mark a provider method as a TRPC mutation procedure
62
- * Mutation procedures are write operations that modify state (POST/PUT/DELETE-like)
63
- *
64
- * @example
65
- * ```typescript
66
- * class CartProvider extends BaseProvider {
67
- * @trpcMutation()
68
- * async add(payload: CartAddMutation, session: Session): Promise<Cart> {
69
- * // implementation
70
- * }
71
- *
72
- * @trpcMutation({ name: 'removeItem' })
73
- * async remove(payload: CartRemoveMutation, session: Session): Promise<Cart> {
74
- * // implementation
75
- * }
76
- * }
77
- * ```
78
- */
79
- export function trpcMutation(options: TRPCMethodOptions = {}): MethodDecorator {
80
- return function (target: any, propertyKey: string | symbol, descriptor: PropertyDescriptor) {
81
- // Store metadata about this method being a TRPC mutation
82
- const metadata = {
83
- methodName: String(propertyKey),
84
- isQuery: false,
85
- isMutation: true,
86
- options
87
- };
88
-
89
- // Store on the prototype so it's inherited
90
- Reflect.defineMetadata(TRPC_MUTATION_METADATA_KEY, metadata, target, propertyKey);
91
-
92
- // Also store a list of all TRPC methods on the class
93
- const existingMethods = Reflect.getMetadata(TRPC_MUTATION_METADATA_KEY, target.constructor) || [];
94
- existingMethods.push({ propertyKey, metadata });
95
- Reflect.defineMetadata(TRPC_MUTATION_METADATA_KEY, existingMethods, target.constructor);
96
- };
97
- }
98
-
99
- /**
100
- * Get all TRPC query methods from a class or instance
101
- */
102
- export function getTRPCQueryMethods(target: any): Array<{ propertyKey: string | symbol; metadata: any }> {
103
- const constructor = typeof target === 'function' ? target : target.constructor;
104
- return Reflect.getMetadata(TRPC_QUERY_METADATA_KEY, constructor) || [];
105
- }
106
-
107
- /**
108
- * Get all TRPC mutation methods from a class or instance
109
- */
110
- export function getTRPCMutationMethods(target: any): Array<{ propertyKey: string | symbol; metadata: any }> {
111
- const constructor = typeof target === 'function' ? target : target.constructor;
112
- return Reflect.getMetadata(TRPC_MUTATION_METADATA_KEY, constructor) || [];
113
- }
114
-
115
- /**
116
- * Get all TRPC methods (both queries and mutations) from a class or instance
117
- */
118
- export function getAllTRPCMethods(target: any): Array<{ propertyKey: string | symbol; metadata: any }> {
119
- return [
120
- ...getTRPCQueryMethods(target),
121
- ...getTRPCMutationMethods(target)
122
- ];
123
- }
124
-
125
- /**
126
- * Check if a method is marked as a TRPC query
127
- */
128
- export function isTRPCQuery(target: any, methodName: string | symbol): boolean {
129
- return !!Reflect.getMetadata(TRPC_QUERY_METADATA_KEY, target, methodName);
130
- }
131
-
132
- /**
133
- * Check if a method is marked as a TRPC mutation
134
- */
135
- export function isTRPCMutation(target: any, methodName: string | symbol): boolean {
136
- return !!Reflect.getMetadata(TRPC_MUTATION_METADATA_KEY, target, methodName);
137
- }
138
-
139
- /**
140
- * Check if a method is marked for TRPC exposure (query or mutation)
141
- */
142
- export function isTRPCMethod(target: any, methodName: string | symbol): boolean {
143
- return isTRPCQuery(target, methodName) || isTRPCMutation(target, methodName);
144
- }
package/otel/src/sdk.ts DELETED
@@ -1,57 +0,0 @@
1
- import { NodeSDK } from '@opentelemetry/sdk-node';
2
-
3
- let sdk: NodeSDK | null = null;
4
- let isInitialized = false;
5
- let initializationPromise: Promise<void> | null = null;
6
-
7
- // Detect if we're running in a browser environment
8
- function isBrowser(): boolean {
9
- return typeof window !== 'undefined' && typeof process === 'undefined';
10
- }
11
-
12
- // Auto-initialize OTEL on first use with standard env vars
13
- function ensureInitialized(): void {
14
- if (isInitialized || initializationPromise) {
15
- return;
16
- }
17
-
18
- // Skip initialization in browser environments
19
- if (isBrowser()) {
20
- isInitialized = true;
21
- return;
22
- }
23
-
24
- // Prevent multiple initialization attempts
25
- initializationPromise = Promise.resolve().then(() => {
26
- // Let NodeSDK handle everything automatically via env vars
27
- // The SDK will automatically pick up OTEL_* environment variables
28
- sdk = new NodeSDK();
29
-
30
- sdk.start();
31
- isInitialized = true;
32
-
33
- process.on('SIGTERM', async () => {
34
- try {
35
- await shutdownOtel();
36
- if (process.env['OTEL_LOG_LEVEL'] === 'debug') {
37
- console.log('OpenTelemetry terminated successfully');
38
- }
39
- } catch (error) {
40
- console.error('Error terminating OpenTelemetry', error);
41
- }
42
- });
43
- });
44
- }
45
-
46
- export async function shutdownOtel(): Promise<void> {
47
- if (sdk) {
48
- await sdk.shutdown();
49
- sdk = null;
50
- isInitialized = false;
51
- }
52
- }
53
-
54
- export function isOtelInitialized(): boolean {
55
- ensureInitialized();
56
- return isInitialized;
57
- }