trickle-observe 0.2.93 → 0.2.94

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.
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Database query observer — patches popular database drivers to capture
3
+ * SQL queries, execution time, and result shapes.
4
+ *
5
+ * Currently supports:
6
+ * - pg (node-postgres) — used by Prisma, Knex, Sequelize, TypeORM
7
+ *
8
+ * Captured data is written to .trickle/queries.jsonl as:
9
+ * { query: "SELECT ...", params: [...], durationMs: 2.5, rowCount: 42, columns: [...] }
10
+ */
11
+ /**
12
+ * Patch pg (node-postgres) to capture queries.
13
+ * Called from observe-register when pg is required.
14
+ */
15
+ export declare function patchPg(pgModule: any, debug: boolean): void;
@@ -0,0 +1,199 @@
1
+ "use strict";
2
+ /**
3
+ * Database query observer — patches popular database drivers to capture
4
+ * SQL queries, execution time, and result shapes.
5
+ *
6
+ * Currently supports:
7
+ * - pg (node-postgres) — used by Prisma, Knex, Sequelize, TypeORM
8
+ *
9
+ * Captured data is written to .trickle/queries.jsonl as:
10
+ * { query: "SELECT ...", params: [...], durationMs: 2.5, rowCount: 42, columns: [...] }
11
+ */
12
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
13
+ if (k2 === undefined) k2 = k;
14
+ var desc = Object.getOwnPropertyDescriptor(m, k);
15
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
16
+ desc = { enumerable: true, get: function() { return m[k]; } };
17
+ }
18
+ Object.defineProperty(o, k2, desc);
19
+ }) : (function(o, m, k, k2) {
20
+ if (k2 === undefined) k2 = k;
21
+ o[k2] = m[k];
22
+ }));
23
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
24
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
25
+ }) : function(o, v) {
26
+ o["default"] = v;
27
+ });
28
+ var __importStar = (this && this.__importStar) || (function () {
29
+ var ownKeys = function(o) {
30
+ ownKeys = Object.getOwnPropertyNames || function (o) {
31
+ var ar = [];
32
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
33
+ return ar;
34
+ };
35
+ return ownKeys(o);
36
+ };
37
+ return function (mod) {
38
+ if (mod && mod.__esModule) return mod;
39
+ var result = {};
40
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
41
+ __setModuleDefault(result, mod);
42
+ return result;
43
+ };
44
+ })();
45
+ Object.defineProperty(exports, "__esModule", { value: true });
46
+ exports.patchPg = patchPg;
47
+ const fs = __importStar(require("fs"));
48
+ const path = __importStar(require("path"));
49
+ let queriesFile = null;
50
+ let debugMode = false;
51
+ const MAX_QUERY_LENGTH = 500;
52
+ const MAX_QUERIES = 100;
53
+ let queryCount = 0;
54
+ function getQueriesFile() {
55
+ if (queriesFile)
56
+ return queriesFile;
57
+ const dir = process.env.TRICKLE_LOCAL_DIR || path.join(process.cwd(), '.trickle');
58
+ try {
59
+ fs.mkdirSync(dir, { recursive: true });
60
+ }
61
+ catch { }
62
+ queriesFile = path.join(dir, 'queries.jsonl');
63
+ // Clear previous
64
+ try {
65
+ fs.writeFileSync(queriesFile, '');
66
+ }
67
+ catch { }
68
+ return queriesFile;
69
+ }
70
+ function writeQuery(record) {
71
+ if (queryCount >= MAX_QUERIES)
72
+ return;
73
+ queryCount++;
74
+ try {
75
+ fs.appendFileSync(getQueriesFile(), JSON.stringify(record) + '\n');
76
+ }
77
+ catch { }
78
+ }
79
+ /**
80
+ * Patch pg (node-postgres) to capture queries.
81
+ * Called from observe-register when pg is required.
82
+ */
83
+ function patchPg(pgModule, debug) {
84
+ debugMode = debug;
85
+ // Patch Client.prototype.query
86
+ const Client = pgModule.Client;
87
+ if (!Client || !Client.prototype)
88
+ return;
89
+ const originalQuery = Client.prototype.query;
90
+ if (originalQuery.__trickle_patched)
91
+ return;
92
+ Client.prototype.query = function patchedQuery(...args) {
93
+ const startTime = performance.now();
94
+ // Extract query text and params
95
+ let queryText = '';
96
+ let params;
97
+ if (typeof args[0] === 'string') {
98
+ queryText = args[0];
99
+ params = Array.isArray(args[1]) ? args[1] : undefined;
100
+ }
101
+ else if (args[0] && typeof args[0] === 'object' && args[0].text) {
102
+ queryText = args[0].text;
103
+ params = args[0].values;
104
+ }
105
+ const truncatedQuery = queryText.length > MAX_QUERY_LENGTH
106
+ ? queryText.substring(0, MAX_QUERY_LENGTH) + '...'
107
+ : queryText;
108
+ // Call original
109
+ const result = originalQuery.apply(this, args);
110
+ // Handle promise-based queries
111
+ if (result && typeof result.then === 'function') {
112
+ return result.then((res) => {
113
+ const durationMs = Math.round((performance.now() - startTime) * 100) / 100;
114
+ const columns = res.fields?.map((f) => f.name) || [];
115
+ writeQuery({
116
+ kind: 'query',
117
+ query: truncatedQuery,
118
+ params: params?.slice(0, 5),
119
+ durationMs,
120
+ rowCount: res.rowCount || 0,
121
+ columns: columns.length > 0 ? columns : undefined,
122
+ timestamp: Date.now(),
123
+ });
124
+ if (debugMode) {
125
+ console.log(`[trickle/db] ${truncatedQuery.substring(0, 60)}... (${durationMs}ms, ${res.rowCount} rows)`);
126
+ }
127
+ return res;
128
+ }, (err) => {
129
+ const durationMs = Math.round((performance.now() - startTime) * 100) / 100;
130
+ writeQuery({
131
+ kind: 'query',
132
+ query: truncatedQuery,
133
+ params: params?.slice(0, 5),
134
+ durationMs,
135
+ rowCount: 0,
136
+ error: err.message?.substring(0, 200),
137
+ timestamp: Date.now(),
138
+ });
139
+ throw err;
140
+ });
141
+ }
142
+ return result;
143
+ };
144
+ Client.prototype.query.__trickle_patched = true;
145
+ // Also patch Pool if available
146
+ if (pgModule.Pool) {
147
+ const Pool = pgModule.Pool;
148
+ const origPoolQuery = Pool.prototype.query;
149
+ if (origPoolQuery && !origPoolQuery.__trickle_patched) {
150
+ Pool.prototype.query = function patchedPoolQuery(...args) {
151
+ const startTime = performance.now();
152
+ let queryText = '';
153
+ let params;
154
+ if (typeof args[0] === 'string') {
155
+ queryText = args[0];
156
+ params = Array.isArray(args[1]) ? args[1] : undefined;
157
+ }
158
+ else if (args[0] && typeof args[0] === 'object' && args[0].text) {
159
+ queryText = args[0].text;
160
+ params = args[0].values;
161
+ }
162
+ const truncatedQuery = queryText.length > MAX_QUERY_LENGTH
163
+ ? queryText.substring(0, MAX_QUERY_LENGTH) + '...'
164
+ : queryText;
165
+ const result = origPoolQuery.apply(this, args);
166
+ if (result && typeof result.then === 'function') {
167
+ return result.then((res) => {
168
+ const durationMs = Math.round((performance.now() - startTime) * 100) / 100;
169
+ writeQuery({
170
+ kind: 'query',
171
+ query: truncatedQuery,
172
+ params: params?.slice(0, 5),
173
+ durationMs,
174
+ rowCount: res.rowCount || 0,
175
+ columns: res.fields?.map((f) => f.name),
176
+ timestamp: Date.now(),
177
+ });
178
+ return res;
179
+ }, (err) => {
180
+ writeQuery({
181
+ kind: 'query',
182
+ query: truncatedQuery,
183
+ durationMs: Math.round((performance.now() - startTime) * 100) / 100,
184
+ rowCount: 0,
185
+ error: err.message?.substring(0, 200),
186
+ timestamp: Date.now(),
187
+ });
188
+ throw err;
189
+ });
190
+ }
191
+ return result;
192
+ };
193
+ Pool.prototype.query.__trickle_patched = true;
194
+ }
195
+ }
196
+ if (debugMode) {
197
+ console.log('[trickle/db] PostgreSQL query tracing enabled');
198
+ }
199
+ }
@@ -1123,6 +1123,15 @@ if (enabled) {
1123
1123
  }
1124
1124
  catch { /* fall through to normal processing */ }
1125
1125
  }
1126
+ // ── PostgreSQL auto-detection: patch pg to capture SQL queries ──
1127
+ if (request === 'pg' && !expressPatched.has('pg')) {
1128
+ expressPatched.add('pg');
1129
+ try {
1130
+ const { patchPg } = require(path_1.default.join(__dirname, 'db-observer.js'));
1131
+ patchPg(exports, debug);
1132
+ }
1133
+ catch { /* pg observer not critical */ }
1134
+ }
1126
1135
  // Resolve to absolute path for dedup — do this FIRST since bundlers like
1127
1136
  // tsx/esbuild may use path aliases (e.g., @config/env) that don't start
1128
1137
  // with './' or '/'. We need the resolved path to decide if it's user code.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "trickle-observe",
3
- "version": "0.2.93",
3
+ "version": "0.2.94",
4
4
  "description": "Runtime type observability for JavaScript applications",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -0,0 +1,185 @@
1
+ /**
2
+ * Database query observer — patches popular database drivers to capture
3
+ * SQL queries, execution time, and result shapes.
4
+ *
5
+ * Currently supports:
6
+ * - pg (node-postgres) — used by Prisma, Knex, Sequelize, TypeORM
7
+ *
8
+ * Captured data is written to .trickle/queries.jsonl as:
9
+ * { query: "SELECT ...", params: [...], durationMs: 2.5, rowCount: 42, columns: [...] }
10
+ */
11
+
12
+ import * as fs from 'fs';
13
+ import * as path from 'path';
14
+
15
+ interface QueryRecord {
16
+ kind: 'query';
17
+ query: string;
18
+ params?: unknown[];
19
+ durationMs: number;
20
+ rowCount: number;
21
+ columns?: string[];
22
+ error?: string;
23
+ timestamp: number;
24
+ }
25
+
26
+ let queriesFile: string | null = null;
27
+ let debugMode = false;
28
+ const MAX_QUERY_LENGTH = 500;
29
+ const MAX_QUERIES = 100;
30
+ let queryCount = 0;
31
+
32
+ function getQueriesFile(): string {
33
+ if (queriesFile) return queriesFile;
34
+ const dir = process.env.TRICKLE_LOCAL_DIR || path.join(process.cwd(), '.trickle');
35
+ try { fs.mkdirSync(dir, { recursive: true }); } catch {}
36
+ queriesFile = path.join(dir, 'queries.jsonl');
37
+ // Clear previous
38
+ try { fs.writeFileSync(queriesFile, ''); } catch {}
39
+ return queriesFile;
40
+ }
41
+
42
+ function writeQuery(record: QueryRecord): void {
43
+ if (queryCount >= MAX_QUERIES) return;
44
+ queryCount++;
45
+ try {
46
+ fs.appendFileSync(getQueriesFile(), JSON.stringify(record) + '\n');
47
+ } catch {}
48
+ }
49
+
50
+ /**
51
+ * Patch pg (node-postgres) to capture queries.
52
+ * Called from observe-register when pg is required.
53
+ */
54
+ export function patchPg(pgModule: any, debug: boolean): void {
55
+ debugMode = debug;
56
+
57
+ // Patch Client.prototype.query
58
+ const Client = pgModule.Client;
59
+ if (!Client || !Client.prototype) return;
60
+
61
+ const originalQuery = Client.prototype.query;
62
+ if ((originalQuery as any).__trickle_patched) return;
63
+
64
+ Client.prototype.query = function patchedQuery(...args: any[]): any {
65
+ const startTime = performance.now();
66
+
67
+ // Extract query text and params
68
+ let queryText = '';
69
+ let params: unknown[] | undefined;
70
+ if (typeof args[0] === 'string') {
71
+ queryText = args[0];
72
+ params = Array.isArray(args[1]) ? args[1] : undefined;
73
+ } else if (args[0] && typeof args[0] === 'object' && args[0].text) {
74
+ queryText = args[0].text;
75
+ params = args[0].values;
76
+ }
77
+
78
+ const truncatedQuery = queryText.length > MAX_QUERY_LENGTH
79
+ ? queryText.substring(0, MAX_QUERY_LENGTH) + '...'
80
+ : queryText;
81
+
82
+ // Call original
83
+ const result = originalQuery.apply(this, args);
84
+
85
+ // Handle promise-based queries
86
+ if (result && typeof result.then === 'function') {
87
+ return result.then(
88
+ (res: any) => {
89
+ const durationMs = Math.round((performance.now() - startTime) * 100) / 100;
90
+ const columns = res.fields?.map((f: any) => f.name) || [];
91
+ writeQuery({
92
+ kind: 'query',
93
+ query: truncatedQuery,
94
+ params: params?.slice(0, 5),
95
+ durationMs,
96
+ rowCount: res.rowCount || 0,
97
+ columns: columns.length > 0 ? columns : undefined,
98
+ timestamp: Date.now(),
99
+ });
100
+ if (debugMode) {
101
+ console.log(`[trickle/db] ${truncatedQuery.substring(0, 60)}... (${durationMs}ms, ${res.rowCount} rows)`);
102
+ }
103
+ return res;
104
+ },
105
+ (err: any) => {
106
+ const durationMs = Math.round((performance.now() - startTime) * 100) / 100;
107
+ writeQuery({
108
+ kind: 'query',
109
+ query: truncatedQuery,
110
+ params: params?.slice(0, 5),
111
+ durationMs,
112
+ rowCount: 0,
113
+ error: err.message?.substring(0, 200),
114
+ timestamp: Date.now(),
115
+ });
116
+ throw err;
117
+ },
118
+ );
119
+ }
120
+
121
+ return result;
122
+ };
123
+
124
+ (Client.prototype.query as any).__trickle_patched = true;
125
+
126
+ // Also patch Pool if available
127
+ if (pgModule.Pool) {
128
+ const Pool = pgModule.Pool;
129
+ const origPoolQuery = Pool.prototype.query;
130
+ if (origPoolQuery && !(origPoolQuery as any).__trickle_patched) {
131
+ Pool.prototype.query = function patchedPoolQuery(...args: any[]): any {
132
+ const startTime = performance.now();
133
+ let queryText = '';
134
+ let params: unknown[] | undefined;
135
+ if (typeof args[0] === 'string') {
136
+ queryText = args[0];
137
+ params = Array.isArray(args[1]) ? args[1] : undefined;
138
+ } else if (args[0] && typeof args[0] === 'object' && args[0].text) {
139
+ queryText = args[0].text;
140
+ params = args[0].values;
141
+ }
142
+
143
+ const truncatedQuery = queryText.length > MAX_QUERY_LENGTH
144
+ ? queryText.substring(0, MAX_QUERY_LENGTH) + '...'
145
+ : queryText;
146
+
147
+ const result = origPoolQuery.apply(this, args);
148
+ if (result && typeof result.then === 'function') {
149
+ return result.then(
150
+ (res: any) => {
151
+ const durationMs = Math.round((performance.now() - startTime) * 100) / 100;
152
+ writeQuery({
153
+ kind: 'query',
154
+ query: truncatedQuery,
155
+ params: params?.slice(0, 5),
156
+ durationMs,
157
+ rowCount: res.rowCount || 0,
158
+ columns: res.fields?.map((f: any) => f.name),
159
+ timestamp: Date.now(),
160
+ });
161
+ return res;
162
+ },
163
+ (err: any) => {
164
+ writeQuery({
165
+ kind: 'query',
166
+ query: truncatedQuery,
167
+ durationMs: Math.round((performance.now() - startTime) * 100) / 100,
168
+ rowCount: 0,
169
+ error: err.message?.substring(0, 200),
170
+ timestamp: Date.now(),
171
+ });
172
+ throw err;
173
+ },
174
+ );
175
+ }
176
+ return result;
177
+ };
178
+ (Pool.prototype.query as any).__trickle_patched = true;
179
+ }
180
+ }
181
+
182
+ if (debugMode) {
183
+ console.log('[trickle/db] PostgreSQL query tracing enabled');
184
+ }
185
+ }
@@ -1113,6 +1113,15 @@ if (enabled) {
1113
1113
  } catch { /* fall through to normal processing */ }
1114
1114
  }
1115
1115
 
1116
+ // ── PostgreSQL auto-detection: patch pg to capture SQL queries ──
1117
+ if (request === 'pg' && !expressPatched.has('pg')) {
1118
+ expressPatched.add('pg');
1119
+ try {
1120
+ const { patchPg } = require(path.join(__dirname, 'db-observer.js'));
1121
+ patchPg(exports, debug);
1122
+ } catch { /* pg observer not critical */ }
1123
+ }
1124
+
1116
1125
  // Resolve to absolute path for dedup — do this FIRST since bundlers like
1117
1126
  // tsx/esbuild may use path aliases (e.g., @config/env) that don't start
1118
1127
  // with './' or '/'. We need the resolved path to decide if it's user code.