trickle-observe 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/auto-env.js +13 -0
- package/auto-esm.mjs +128 -0
- package/auto.js +3 -0
- package/dist/auto-codegen.d.ts +29 -0
- package/dist/auto-codegen.js +999 -0
- package/dist/auto-register.d.ts +16 -0
- package/dist/auto-register.js +99 -0
- package/dist/cache.d.ts +27 -0
- package/dist/cache.js +52 -0
- package/dist/env-detect.d.ts +5 -0
- package/dist/env-detect.js +35 -0
- package/dist/express.d.ts +44 -0
- package/dist/express.js +342 -0
- package/dist/fetch-observer.d.ts +24 -0
- package/dist/fetch-observer.js +217 -0
- package/dist/index.d.ts +64 -0
- package/dist/index.js +172 -0
- package/dist/observe-register.d.ts +29 -0
- package/dist/observe-register.js +455 -0
- package/dist/observe.d.ts +44 -0
- package/dist/observe.js +109 -0
- package/dist/proxy-tracker.d.ts +15 -0
- package/dist/proxy-tracker.js +172 -0
- package/dist/register.d.ts +21 -0
- package/dist/register.js +105 -0
- package/dist/transport.d.ts +22 -0
- package/dist/transport.js +228 -0
- package/dist/type-hash.d.ts +5 -0
- package/dist/type-hash.js +60 -0
- package/dist/type-inference.d.ts +14 -0
- package/dist/type-inference.js +259 -0
- package/dist/types.d.ts +78 -0
- package/dist/types.js +2 -0
- package/dist/wrap.d.ts +10 -0
- package/dist/wrap.js +247 -0
- package/observe-esm-hooks.mjs +367 -0
- package/observe-esm.mjs +40 -0
- package/observe.js +2 -0
- package/package.json +26 -0
- package/register.js +2 -0
- package/src/auto-codegen.ts +1058 -0
- package/src/auto-register.ts +102 -0
- package/src/cache.ts +53 -0
- package/src/env-detect.ts +22 -0
- package/src/express.ts +386 -0
- package/src/fetch-observer.ts +226 -0
- package/src/index.ts +199 -0
- package/src/observe-register.ts +453 -0
- package/src/observe.ts +127 -0
- package/src/proxy-tracker.ts +208 -0
- package/src/register.ts +110 -0
- package/src/transport.ts +207 -0
- package/src/type-hash.ts +71 -0
- package/src/type-inference.ts +285 -0
- package/src/types.ts +61 -0
- package/src/wrap.ts +289 -0
- package/tsconfig.json +8 -0
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fetch observer — patches global.fetch to automatically capture
|
|
3
|
+
* request/response types from HTTP calls made by user code.
|
|
4
|
+
*
|
|
5
|
+
* When your app does:
|
|
6
|
+
* const data = await fetch('https://api.example.com/users').then(r => r.json());
|
|
7
|
+
*
|
|
8
|
+
* Trickle captures:
|
|
9
|
+
* - Function name: "GET /users" (method + path)
|
|
10
|
+
* - Module: "api.example.com" (hostname)
|
|
11
|
+
* - Input type: request body (for POST/PUT/PATCH)
|
|
12
|
+
* - Return type: inferred from JSON response
|
|
13
|
+
* - Sample data: actual response payload
|
|
14
|
+
*
|
|
15
|
+
* The observer only intercepts when .json() is called, so:
|
|
16
|
+
* - Non-JSON responses (HTML, binary) are ignored
|
|
17
|
+
* - The original response is never modified
|
|
18
|
+
* - No extra network requests are made
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { TypeNode, IngestPayload } from './types';
|
|
22
|
+
import { inferType } from './type-inference';
|
|
23
|
+
import { hashType } from './type-hash';
|
|
24
|
+
import { enqueue } from './transport';
|
|
25
|
+
|
|
26
|
+
// Track which type hashes we've already sent to avoid duplicates
|
|
27
|
+
const sentHashes = new Set<string>();
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Patch global.fetch to observe JSON responses.
|
|
31
|
+
* Safe to call multiple times (idempotent).
|
|
32
|
+
*/
|
|
33
|
+
export function patchFetch(environment: string, debugMode: boolean): void {
|
|
34
|
+
if (typeof globalThis.fetch !== 'function') return;
|
|
35
|
+
|
|
36
|
+
const originalFetch = globalThis.fetch;
|
|
37
|
+
|
|
38
|
+
// Guard against double-patching
|
|
39
|
+
if ((originalFetch as any).__trickle_patched) return;
|
|
40
|
+
|
|
41
|
+
globalThis.fetch = async function trickleObservedFetch(
|
|
42
|
+
input: any,
|
|
43
|
+
init?: any,
|
|
44
|
+
): Promise<Response> {
|
|
45
|
+
// Extract URL and method before the request
|
|
46
|
+
let url: string;
|
|
47
|
+
let method: string;
|
|
48
|
+
|
|
49
|
+
if (typeof input === 'string') {
|
|
50
|
+
url = input;
|
|
51
|
+
method = init?.method?.toUpperCase() || 'GET';
|
|
52
|
+
} else if (input instanceof URL) {
|
|
53
|
+
url = input.href;
|
|
54
|
+
method = init?.method?.toUpperCase() || 'GET';
|
|
55
|
+
} else if (typeof input === 'object' && input !== null && 'url' in input) {
|
|
56
|
+
// Request object
|
|
57
|
+
url = (input as any).url;
|
|
58
|
+
method = (init?.method || (input as any).method || 'GET').toUpperCase();
|
|
59
|
+
} else {
|
|
60
|
+
url = String(input);
|
|
61
|
+
method = init?.method?.toUpperCase() || 'GET';
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Skip trickle's own backend calls
|
|
65
|
+
if (url.includes('/api/ingest') || url.includes('/api/functions') || url.includes('/api/health')) {
|
|
66
|
+
return originalFetch.call(globalThis, input, init);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Make the actual request
|
|
70
|
+
const response = await originalFetch.call(globalThis, input, init);
|
|
71
|
+
|
|
72
|
+
// Only observe JSON responses
|
|
73
|
+
const contentType = response.headers.get('content-type') || '';
|
|
74
|
+
if (!contentType.includes('json')) {
|
|
75
|
+
return response;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Clone and intercept: read the clone's JSON in background
|
|
79
|
+
try {
|
|
80
|
+
const cloned = response.clone();
|
|
81
|
+
cloned.json().then((data: any) => {
|
|
82
|
+
try {
|
|
83
|
+
captureHttpResponse(method, url, init?.body, data, environment, debugMode);
|
|
84
|
+
} catch {
|
|
85
|
+
// Never interfere
|
|
86
|
+
}
|
|
87
|
+
}).catch(() => {});
|
|
88
|
+
} catch {
|
|
89
|
+
// Clone/read failed — ignore
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return response;
|
|
93
|
+
} as typeof fetch;
|
|
94
|
+
|
|
95
|
+
// Mark as patched
|
|
96
|
+
(globalThis.fetch as any).__trickle_patched = true;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Parse a URL into a clean function name and module name.
|
|
101
|
+
* "https://api.example.com/v1/users?limit=10"
|
|
102
|
+
* → functionName: "GET /v1/users", module: "api.example.com"
|
|
103
|
+
*/
|
|
104
|
+
function parseUrl(method: string, rawUrl: string): { functionName: string; module: string } {
|
|
105
|
+
try {
|
|
106
|
+
const parsed = new URL(rawUrl);
|
|
107
|
+
const pathname = parsed.pathname || '/';
|
|
108
|
+
return {
|
|
109
|
+
functionName: `${method} ${pathname}`,
|
|
110
|
+
module: parsed.hostname || 'http',
|
|
111
|
+
};
|
|
112
|
+
} catch {
|
|
113
|
+
// Relative URL or invalid — use as-is
|
|
114
|
+
return {
|
|
115
|
+
functionName: `${method} ${rawUrl}`,
|
|
116
|
+
module: 'http',
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Capture the HTTP response type and enqueue it to the backend.
|
|
123
|
+
*/
|
|
124
|
+
function captureHttpResponse(
|
|
125
|
+
method: string,
|
|
126
|
+
url: string,
|
|
127
|
+
requestBody: any,
|
|
128
|
+
responseData: unknown,
|
|
129
|
+
environment: string,
|
|
130
|
+
debugMode: boolean,
|
|
131
|
+
): void {
|
|
132
|
+
const { functionName, module: moduleName } = parseUrl(method, url);
|
|
133
|
+
|
|
134
|
+
// Infer types
|
|
135
|
+
const returnType = inferType(responseData, 5);
|
|
136
|
+
|
|
137
|
+
// Infer request body type (for POST/PUT/PATCH)
|
|
138
|
+
let argsType: TypeNode;
|
|
139
|
+
if (requestBody && typeof requestBody === 'string') {
|
|
140
|
+
try {
|
|
141
|
+
const parsed = JSON.parse(requestBody);
|
|
142
|
+
argsType = { kind: 'tuple', elements: [inferType(parsed, 5)] };
|
|
143
|
+
} catch {
|
|
144
|
+
argsType = { kind: 'tuple', elements: [] };
|
|
145
|
+
}
|
|
146
|
+
} else {
|
|
147
|
+
argsType = { kind: 'tuple', elements: [] };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const hash = hashType(argsType, returnType);
|
|
151
|
+
|
|
152
|
+
// Dedup — only send each unique type shape once
|
|
153
|
+
const key = `${functionName}::${hash}`;
|
|
154
|
+
if (sentHashes.has(key)) return;
|
|
155
|
+
sentHashes.add(key);
|
|
156
|
+
|
|
157
|
+
// Build sample input from request body
|
|
158
|
+
let sampleInput: unknown = undefined;
|
|
159
|
+
if (requestBody && typeof requestBody === 'string') {
|
|
160
|
+
try {
|
|
161
|
+
sampleInput = JSON.parse(requestBody);
|
|
162
|
+
} catch {
|
|
163
|
+
sampleInput = undefined;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const payload: IngestPayload = {
|
|
168
|
+
functionName,
|
|
169
|
+
module: moduleName,
|
|
170
|
+
language: 'js',
|
|
171
|
+
environment,
|
|
172
|
+
typeHash: hash,
|
|
173
|
+
argsType,
|
|
174
|
+
returnType,
|
|
175
|
+
sampleInput: sampleInput ? [sampleInput] : undefined,
|
|
176
|
+
sampleOutput: sanitizeSample(responseData),
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
enqueue(payload);
|
|
180
|
+
|
|
181
|
+
if (debugMode) {
|
|
182
|
+
console.log(`[trickle/fetch] Captured ${functionName} → ${describeType(returnType)}`);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Brief description of a type for debug logging.
|
|
188
|
+
*/
|
|
189
|
+
function describeType(type: TypeNode): string {
|
|
190
|
+
if (type.kind === 'object') {
|
|
191
|
+
const props = Object.keys(type.properties || {});
|
|
192
|
+
if (props.length <= 4) return `{ ${props.join(', ')} }`;
|
|
193
|
+
return `{ ${props.slice(0, 3).join(', ')}, ... } (${props.length} props)`;
|
|
194
|
+
}
|
|
195
|
+
if (type.kind === 'array') return `${describeType(type.element)}[]`;
|
|
196
|
+
if (type.kind === 'primitive') return type.name;
|
|
197
|
+
return type.kind;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Sanitize sample data for storage (truncate large values).
|
|
202
|
+
*/
|
|
203
|
+
function sanitizeSample(value: unknown, depth: number = 3): unknown {
|
|
204
|
+
if (depth <= 0) return '[truncated]';
|
|
205
|
+
if (value === null || value === undefined) return value;
|
|
206
|
+
const t = typeof value;
|
|
207
|
+
if (t === 'string') {
|
|
208
|
+
const s = value as string;
|
|
209
|
+
return s.length > 200 ? s.substring(0, 200) + '...' : s;
|
|
210
|
+
}
|
|
211
|
+
if (t === 'number' || t === 'boolean') return value;
|
|
212
|
+
if (t === 'function') return '[Function]';
|
|
213
|
+
if (Array.isArray(value)) {
|
|
214
|
+
return value.slice(0, 5).map(item => sanitizeSample(item, depth - 1));
|
|
215
|
+
}
|
|
216
|
+
if (t === 'object') {
|
|
217
|
+
const obj = value as Record<string, unknown>;
|
|
218
|
+
const result: Record<string, unknown> = {};
|
|
219
|
+
const keys = Object.keys(obj).slice(0, 20);
|
|
220
|
+
for (const key of keys) {
|
|
221
|
+
try { result[key] = sanitizeSample(obj[key], depth - 1); } catch { result[key] = '[unreadable]'; }
|
|
222
|
+
}
|
|
223
|
+
return result;
|
|
224
|
+
}
|
|
225
|
+
return String(value);
|
|
226
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { configure as configureTransport, flush } from './transport';
|
|
2
|
+
import { wrapFunction } from './wrap';
|
|
3
|
+
import { detectEnvironment } from './env-detect';
|
|
4
|
+
import { GlobalOpts, TrickleOpts, WrapOptions } from './types';
|
|
5
|
+
import { instrumentExpress, trickleMiddleware } from './express';
|
|
6
|
+
|
|
7
|
+
let globalOpts: GlobalOpts = {
|
|
8
|
+
backendUrl: 'http://localhost:4888',
|
|
9
|
+
batchIntervalMs: 2000,
|
|
10
|
+
enabled: true,
|
|
11
|
+
environment: undefined,
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Configure trickle global options.
|
|
16
|
+
* Call this before wrapping any functions if you need non-default settings.
|
|
17
|
+
*/
|
|
18
|
+
export function configure(opts: Partial<GlobalOpts>): void {
|
|
19
|
+
Object.assign(globalOpts, opts);
|
|
20
|
+
configureTransport(globalOpts as GlobalOpts);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Wrap a function to capture runtime type information.
|
|
25
|
+
*
|
|
26
|
+
* Usage:
|
|
27
|
+
* const wrapped = trickle(myFunction);
|
|
28
|
+
* const wrapped = trickle(myFunction, { name: 'myFn', module: 'api' });
|
|
29
|
+
* const wrapped = trickle('myFunction', myFunction);
|
|
30
|
+
* const wrapped = trickle('myFunction', myFunction, { module: 'api' });
|
|
31
|
+
*/
|
|
32
|
+
export function trickle<T extends (...args: any[]) => any>(fn: T, opts?: TrickleOpts): T;
|
|
33
|
+
export function trickle<T extends (...args: any[]) => any>(name: string, fn: T, opts?: TrickleOpts): T;
|
|
34
|
+
export function trickle(...args: any[]): any {
|
|
35
|
+
let fn: (...args: any[]) => any;
|
|
36
|
+
let opts: TrickleOpts = {};
|
|
37
|
+
let explicitName: string | undefined;
|
|
38
|
+
|
|
39
|
+
if (typeof args[0] === 'string') {
|
|
40
|
+
explicitName = args[0];
|
|
41
|
+
fn = args[1];
|
|
42
|
+
opts = args[2] || {};
|
|
43
|
+
} else {
|
|
44
|
+
fn = args[0];
|
|
45
|
+
opts = args[1] || {};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (typeof fn !== 'function') {
|
|
49
|
+
throw new TypeError('trickle: expected a function argument');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const functionName = explicitName || opts.name || fn.name || 'anonymous';
|
|
53
|
+
const module = opts.module || inferModule();
|
|
54
|
+
const environment = globalOpts.environment || detectEnvironment();
|
|
55
|
+
|
|
56
|
+
const wrapOpts: WrapOptions = {
|
|
57
|
+
functionName,
|
|
58
|
+
module,
|
|
59
|
+
trackArgs: opts.trackArgs !== false,
|
|
60
|
+
trackReturn: opts.trackReturn !== false,
|
|
61
|
+
sampleRate: opts.sampleRate ?? 1,
|
|
62
|
+
maxDepth: opts.maxDepth ?? 5,
|
|
63
|
+
environment,
|
|
64
|
+
enabled: globalOpts.enabled,
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
return wrapFunction(fn, wrapOpts);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Wrap a Lambda handler function.
|
|
72
|
+
* Same as trickle() but automatically flushes the transport after each invocation,
|
|
73
|
+
* since Lambda may freeze the process between invocations.
|
|
74
|
+
*/
|
|
75
|
+
export function trickleHandler<T extends (...args: any[]) => any>(handler: T, opts?: TrickleOpts): T {
|
|
76
|
+
const wrapped = trickle(handler, {
|
|
77
|
+
...opts,
|
|
78
|
+
name: opts?.name || handler.name || 'handler',
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
const flushing = function (this: any, ...args: any[]): any {
|
|
82
|
+
const result = wrapped.apply(this, args);
|
|
83
|
+
|
|
84
|
+
// If the handler returns a promise, flush after it resolves
|
|
85
|
+
if (result !== null && result !== undefined && typeof result === 'object' && typeof result.then === 'function') {
|
|
86
|
+
return result.then(
|
|
87
|
+
async (resolved: unknown) => {
|
|
88
|
+
await flush().catch(() => {});
|
|
89
|
+
return resolved;
|
|
90
|
+
},
|
|
91
|
+
async (err: unknown) => {
|
|
92
|
+
await flush().catch(() => {});
|
|
93
|
+
throw err;
|
|
94
|
+
},
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Synchronous handler — flush and return
|
|
99
|
+
flush().catch(() => {});
|
|
100
|
+
return result;
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
Object.defineProperty(flushing, 'name', { value: handler.name || 'handler', configurable: true });
|
|
104
|
+
Object.defineProperty(flushing, 'length', { value: handler.length, configurable: true });
|
|
105
|
+
|
|
106
|
+
return flushing as unknown as T;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Instrument an Express app by monkey-patching route methods to capture types.
|
|
111
|
+
*
|
|
112
|
+
* Must be called BEFORE defining routes:
|
|
113
|
+
*
|
|
114
|
+
* const app = express();
|
|
115
|
+
* trickleExpress(app);
|
|
116
|
+
* app.get('/api/users', (req, res) => { ... });
|
|
117
|
+
*
|
|
118
|
+
* Each handler is wrapped to capture:
|
|
119
|
+
* - Input: `{ body, params, query }` from the request
|
|
120
|
+
* - Output: data passed to `res.json()` or `res.send()`
|
|
121
|
+
* - Errors: thrown exceptions or `next(err)` calls
|
|
122
|
+
*/
|
|
123
|
+
export function trickleExpress(
|
|
124
|
+
app: any,
|
|
125
|
+
opts?: { enabled?: boolean; environment?: string; sampleRate?: number; maxDepth?: number },
|
|
126
|
+
): void {
|
|
127
|
+
instrumentExpress(app, {
|
|
128
|
+
enabled: opts?.enabled ?? globalOpts.enabled,
|
|
129
|
+
environment: opts?.environment ?? globalOpts.environment ?? detectEnvironment(),
|
|
130
|
+
sampleRate: opts?.sampleRate ?? 1,
|
|
131
|
+
maxDepth: opts?.maxDepth ?? 5,
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Auto-instrument a framework app. Currently supports Express.
|
|
137
|
+
*
|
|
138
|
+
* Usage:
|
|
139
|
+
* import { instrument } from 'trickle';
|
|
140
|
+
* const app = express();
|
|
141
|
+
* instrument(app);
|
|
142
|
+
*
|
|
143
|
+
* Detects Express by checking for `app.listen` and `app.get` (function) on the object.
|
|
144
|
+
*/
|
|
145
|
+
export function instrument(
|
|
146
|
+
app: any,
|
|
147
|
+
opts?: { enabled?: boolean; environment?: string; sampleRate?: number; maxDepth?: number },
|
|
148
|
+
): void {
|
|
149
|
+
// Detect Express-like app
|
|
150
|
+
if (app && typeof app.listen === 'function' && typeof app.get === 'function' && typeof app.use === 'function') {
|
|
151
|
+
trickleExpress(app, opts);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Future: detect other frameworks here (Koa, Fastify, etc.)
|
|
156
|
+
if (typeof console !== 'undefined' && console.warn) {
|
|
157
|
+
console.warn('[trickle] instrument(): could not detect a supported framework on the provided object');
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Attempt to infer the module name from the call stack.
|
|
163
|
+
* Falls back to 'unknown' if we can't determine it.
|
|
164
|
+
*/
|
|
165
|
+
function inferModule(): string {
|
|
166
|
+
try {
|
|
167
|
+
const stack = new Error().stack;
|
|
168
|
+
if (!stack) return 'unknown';
|
|
169
|
+
|
|
170
|
+
const lines = stack.split('\n');
|
|
171
|
+
// Skip first 3 lines: "Error", trickle internals
|
|
172
|
+
for (let i = 3; i < lines.length; i++) {
|
|
173
|
+
const line = lines[i].trim();
|
|
174
|
+
// Look for a file path
|
|
175
|
+
const match = line.match(/(?:at\s+)?(?:.*?\s+\()?(.+?)(?::\d+:\d+)?\)?$/);
|
|
176
|
+
if (match) {
|
|
177
|
+
let filePath = match[1];
|
|
178
|
+
// Strip node_modules paths
|
|
179
|
+
if (filePath.includes('node_modules')) continue;
|
|
180
|
+
// Extract just the filename or relative path
|
|
181
|
+
const parts = filePath.split('/');
|
|
182
|
+
const filename = parts[parts.length - 1];
|
|
183
|
+
if (filename && !filename.startsWith('<')) {
|
|
184
|
+
return filename.replace(/\.[jt]sx?$/, '');
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
} catch {
|
|
189
|
+
// Don't crash on stack inspection failure
|
|
190
|
+
}
|
|
191
|
+
return 'unknown';
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Re-export public types
|
|
195
|
+
export type { TypeNode, GlobalOpts, TrickleOpts, IngestPayload } from './types';
|
|
196
|
+
export { flush } from './transport';
|
|
197
|
+
export { instrumentExpress, trickleMiddleware } from './express';
|
|
198
|
+
export { observe, observeFn } from './observe';
|
|
199
|
+
export type { ObserveOpts } from './observe';
|