trickle-observe 0.2.104 → 0.2.106

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/transport.js CHANGED
@@ -54,6 +54,13 @@ let localFilePath = '';
54
54
  let queue = [];
55
55
  let flushTimer = null;
56
56
  let isFlushing = false;
57
+ // Cloud streaming: when TRICKLE_CLOUD_URL + TRICKLE_CLOUD_TOKEN are set,
58
+ // observations are also streamed to the cloud backend in real-time.
59
+ let cloudUrl = process.env.TRICKLE_CLOUD_URL || '';
60
+ let cloudToken = process.env.TRICKLE_CLOUD_TOKEN || '';
61
+ let cloudProject = '';
62
+ let cloudBuffer = [];
63
+ let cloudFlushTimer = null;
57
64
  /**
58
65
  * Configure the transport layer with global options.
59
66
  */
@@ -63,6 +70,34 @@ function configure(opts) {
63
70
  maxBatchSize = opts.maxBatchSize || DEFAULT_MAX_BATCH_SIZE;
64
71
  enabled = opts.enabled !== false;
65
72
  debug = opts.debug === true;
73
+ // Load cloud config from ~/.trickle/cloud.json if env vars not set
74
+ if (!cloudUrl || !cloudToken) {
75
+ try {
76
+ const configPath = pathMod.join(process.env.HOME || '~', '.trickle', 'cloud.json');
77
+ if (fs.existsSync(configPath)) {
78
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
79
+ if (!cloudUrl && config.url)
80
+ cloudUrl = config.url;
81
+ if (!cloudToken && config.token)
82
+ cloudToken = config.token;
83
+ }
84
+ }
85
+ catch { }
86
+ }
87
+ cloudProject = process.env.TRICKLE_CLOUD_PROJECT || pathMod.basename(process.cwd());
88
+ // Start cloud streaming if configured
89
+ if (cloudUrl && cloudToken && !cloudFlushTimer) {
90
+ cloudFlushTimer = setInterval(() => {
91
+ flushCloud().catch(() => { });
92
+ }, 5000); // Flush to cloud every 5 seconds
93
+ if (cloudFlushTimer.unref)
94
+ cloudFlushTimer.unref();
95
+ if (debug) {
96
+ console.log(`[trickle] Cloud streaming enabled → ${cloudUrl}`);
97
+ }
98
+ // Flush cloud buffer on exit
99
+ process.on('beforeExit', () => { flushCloud().catch(() => { }); });
100
+ }
66
101
  // Check for local/file-based mode
67
102
  if (process.env.TRICKLE_LOCAL === '1') {
68
103
  localMode = true;
@@ -93,7 +128,12 @@ function enqueue(payload) {
93
128
  // Local file mode: append directly to JSONL file
94
129
  if (localMode && localFilePath) {
95
130
  try {
96
- fs.appendFileSync(localFilePath, JSON.stringify(payload) + '\n');
131
+ const line = JSON.stringify(payload) + '\n';
132
+ fs.appendFileSync(localFilePath, line);
133
+ // Also buffer for cloud if configured
134
+ if (cloudUrl && cloudToken) {
135
+ cloudBuffer.push(line);
136
+ }
97
137
  }
98
138
  catch {
99
139
  // Never crash user's app
@@ -200,6 +240,32 @@ function stopTimer() {
200
240
  function sleep(ms) {
201
241
  return new Promise(resolve => setTimeout(resolve, ms));
202
242
  }
243
+ /**
244
+ * Flush buffered observations to the cloud backend.
245
+ */
246
+ async function flushCloud() {
247
+ if (cloudBuffer.length === 0 || !cloudUrl || !cloudToken)
248
+ return;
249
+ const lines = cloudBuffer.splice(0);
250
+ try {
251
+ await fetch(`${cloudUrl}/api/v1/ingest`, {
252
+ method: 'POST',
253
+ headers: {
254
+ 'Content-Type': 'application/json',
255
+ 'Authorization': `Bearer ${cloudToken}`,
256
+ },
257
+ body: JSON.stringify({
258
+ project: cloudProject,
259
+ file: 'observations.jsonl',
260
+ lines: lines.join(''),
261
+ }),
262
+ signal: AbortSignal.timeout(10000),
263
+ });
264
+ }
265
+ catch {
266
+ // Silent — never crash user's app. Data is still saved locally.
267
+ }
268
+ }
203
269
  function silentError() {
204
270
  // Intentionally empty — never crash user's app
205
271
  }
@@ -153,6 +153,23 @@ function inferObject(obj, depth, seen) {
153
153
  },
154
154
  };
155
155
  }
156
+ // Known complex framework objects — use class name instead of deep introspection.
157
+ // These types have dozens of internal properties that generate unusably verbose stubs.
158
+ const className = obj.constructor?.name;
159
+ const OPAQUE_CLASSES = new Set([
160
+ // Node.js HTTP internals
161
+ 'IncomingMessage', 'ServerResponse', 'Socket', 'Server',
162
+ 'ReadableState', 'WritableState',
163
+ // Express
164
+ 'IncomingMessage', // Express req extends this
165
+ // Streams
166
+ 'Readable', 'Writable', 'Duplex', 'Transform', 'PassThrough',
167
+ // EventEmitter
168
+ 'EventEmitter',
169
+ ]);
170
+ if (className && OPAQUE_CLASSES.has(className)) {
171
+ return { kind: 'object', properties: {}, class_name: className };
172
+ }
156
173
  // Plain objects
157
174
  return inferPlainObject(obj, depth, seen);
158
175
  }
@@ -167,7 +184,7 @@ function inferArray(arr, depth, seen) {
167
184
  }
168
185
  return { kind: 'array', element: unifyTypes(elementTypes) };
169
186
  }
170
- const MAX_PROPERTIES = 20;
187
+ const MAX_PROPERTIES = 12;
171
188
  function inferPlainObject(obj, depth, seen) {
172
189
  const properties = {};
173
190
  const keys = Object.keys(obj);
package/dist/types.d.ts CHANGED
@@ -7,6 +7,7 @@ export type TypeNode = {
7
7
  } | {
8
8
  kind: "object";
9
9
  properties: Record<string, TypeNode>;
10
+ class_name?: string;
10
11
  } | {
11
12
  kind: "union";
12
13
  members: TypeNode[];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "trickle-observe",
3
- "version": "0.2.104",
3
+ "version": "0.2.106",
4
4
  "description": "Runtime type observability for JavaScript applications",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
package/src/transport.ts CHANGED
@@ -19,6 +19,14 @@ let queue: IngestPayload[] = [];
19
19
  let flushTimer: ReturnType<typeof setInterval> | null = null;
20
20
  let isFlushing = false;
21
21
 
22
+ // Cloud streaming: when TRICKLE_CLOUD_URL + TRICKLE_CLOUD_TOKEN are set,
23
+ // observations are also streamed to the cloud backend in real-time.
24
+ let cloudUrl = process.env.TRICKLE_CLOUD_URL || '';
25
+ let cloudToken = process.env.TRICKLE_CLOUD_TOKEN || '';
26
+ let cloudProject = '';
27
+ let cloudBuffer: string[] = [];
28
+ let cloudFlushTimer: ReturnType<typeof setInterval> | null = null;
29
+
22
30
  /**
23
31
  * Configure the transport layer with global options.
24
32
  */
@@ -29,6 +37,33 @@ export function configure(opts: GlobalOpts): void {
29
37
  enabled = opts.enabled !== false;
30
38
  debug = opts.debug === true;
31
39
 
40
+ // Load cloud config from ~/.trickle/cloud.json if env vars not set
41
+ if (!cloudUrl || !cloudToken) {
42
+ try {
43
+ const configPath = pathMod.join(process.env.HOME || '~', '.trickle', 'cloud.json');
44
+ if (fs.existsSync(configPath)) {
45
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
46
+ if (!cloudUrl && config.url) cloudUrl = config.url;
47
+ if (!cloudToken && config.token) cloudToken = config.token;
48
+ }
49
+ } catch {}
50
+ }
51
+ cloudProject = process.env.TRICKLE_CLOUD_PROJECT || pathMod.basename(process.cwd());
52
+
53
+ // Start cloud streaming if configured
54
+ if (cloudUrl && cloudToken && !cloudFlushTimer) {
55
+ cloudFlushTimer = setInterval(() => {
56
+ flushCloud().catch(() => {});
57
+ }, 5000); // Flush to cloud every 5 seconds
58
+ if (cloudFlushTimer.unref) cloudFlushTimer.unref();
59
+ if (debug) {
60
+ console.log(`[trickle] Cloud streaming enabled → ${cloudUrl}`);
61
+ }
62
+
63
+ // Flush cloud buffer on exit
64
+ process.on('beforeExit', () => { flushCloud().catch(() => {}); });
65
+ }
66
+
32
67
  // Check for local/file-based mode
33
68
  if (process.env.TRICKLE_LOCAL === '1') {
34
69
  localMode = true;
@@ -58,7 +93,12 @@ export function enqueue(payload: IngestPayload): void {
58
93
  // Local file mode: append directly to JSONL file
59
94
  if (localMode && localFilePath) {
60
95
  try {
61
- fs.appendFileSync(localFilePath, JSON.stringify(payload) + '\n');
96
+ const line = JSON.stringify(payload) + '\n';
97
+ fs.appendFileSync(localFilePath, line);
98
+ // Also buffer for cloud if configured
99
+ if (cloudUrl && cloudToken) {
100
+ cloudBuffer.push(line);
101
+ }
62
102
  } catch {
63
103
  // Never crash user's app
64
104
  }
@@ -175,6 +215,32 @@ function sleep(ms: number): Promise<void> {
175
215
  return new Promise(resolve => setTimeout(resolve, ms));
176
216
  }
177
217
 
218
+ /**
219
+ * Flush buffered observations to the cloud backend.
220
+ */
221
+ async function flushCloud(): Promise<void> {
222
+ if (cloudBuffer.length === 0 || !cloudUrl || !cloudToken) return;
223
+
224
+ const lines = cloudBuffer.splice(0);
225
+ try {
226
+ await fetch(`${cloudUrl}/api/v1/ingest`, {
227
+ method: 'POST',
228
+ headers: {
229
+ 'Content-Type': 'application/json',
230
+ 'Authorization': `Bearer ${cloudToken}`,
231
+ },
232
+ body: JSON.stringify({
233
+ project: cloudProject,
234
+ file: 'observations.jsonl',
235
+ lines: lines.join(''),
236
+ }),
237
+ signal: AbortSignal.timeout(10000),
238
+ });
239
+ } catch {
240
+ // Silent — never crash user's app. Data is still saved locally.
241
+ }
242
+ }
243
+
178
244
  function silentError(): void {
179
245
  // Intentionally empty — never crash user's app
180
246
  }
@@ -168,6 +168,24 @@ function inferObject(obj: object, depth: number, seen: WeakSet<object>): TypeNod
168
168
  };
169
169
  }
170
170
 
171
+ // Known complex framework objects — use class name instead of deep introspection.
172
+ // These types have dozens of internal properties that generate unusably verbose stubs.
173
+ const className = obj.constructor?.name;
174
+ const OPAQUE_CLASSES = new Set([
175
+ // Node.js HTTP internals
176
+ 'IncomingMessage', 'ServerResponse', 'Socket', 'Server',
177
+ 'ReadableState', 'WritableState',
178
+ // Express
179
+ 'IncomingMessage', // Express req extends this
180
+ // Streams
181
+ 'Readable', 'Writable', 'Duplex', 'Transform', 'PassThrough',
182
+ // EventEmitter
183
+ 'EventEmitter',
184
+ ]);
185
+ if (className && OPAQUE_CLASSES.has(className)) {
186
+ return { kind: 'object', properties: {}, class_name: className };
187
+ }
188
+
171
189
  // Plain objects
172
190
  return inferPlainObject(obj, depth, seen);
173
191
  }
@@ -187,7 +205,7 @@ function inferArray(arr: unknown[], depth: number, seen: WeakSet<object>): TypeN
187
205
  return { kind: 'array', element: unifyTypes(elementTypes) };
188
206
  }
189
207
 
190
- const MAX_PROPERTIES = 20;
208
+ const MAX_PROPERTIES = 12;
191
209
 
192
210
  function inferPlainObject(obj: object, depth: number, seen: WeakSet<object>): TypeNode {
193
211
  const properties: Record<string, TypeNode> = {};
package/src/types.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  export type TypeNode =
2
2
  | { kind: "primitive"; name: "string" | "number" | "boolean" | "null" | "undefined" | "bigint" | "symbol" }
3
3
  | { kind: "array"; element: TypeNode }
4
- | { kind: "object"; properties: Record<string, TypeNode> }
4
+ | { kind: "object"; properties: Record<string, TypeNode>; class_name?: string }
5
5
  | { kind: "union"; members: TypeNode[] }
6
6
  | { kind: "function"; params: TypeNode[]; returnType: TypeNode }
7
7
  | { kind: "promise"; resolved: TypeNode }