fg-tracker-sdk 0.11.0 → 0.14.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.
Files changed (69) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/README.md +88 -34
  3. package/dist/context.d.ts +23 -2
  4. package/dist/context.d.ts.map +1 -1
  5. package/dist/context.js +29 -5
  6. package/dist/context.js.map +1 -1
  7. package/dist/database.d.ts.map +1 -1
  8. package/dist/database.js +3 -1
  9. package/dist/database.js.map +1 -1
  10. package/dist/express.js +1 -1
  11. package/dist/express.js.map +1 -1
  12. package/dist/fetch.d.ts.map +1 -1
  13. package/dist/fetch.js +31 -2
  14. package/dist/fetch.js.map +1 -1
  15. package/dist/genai/bedrock.d.ts +22 -0
  16. package/dist/genai/bedrock.d.ts.map +1 -0
  17. package/dist/genai/bedrock.js +161 -0
  18. package/dist/genai/bedrock.js.map +1 -0
  19. package/dist/genai/call.d.ts +96 -0
  20. package/dist/genai/call.d.ts.map +1 -0
  21. package/dist/genai/call.js +260 -0
  22. package/dist/genai/call.js.map +1 -0
  23. package/dist/genai/config.d.ts +71 -0
  24. package/dist/genai/config.d.ts.map +1 -0
  25. package/dist/genai/config.js +110 -0
  26. package/dist/genai/config.js.map +1 -0
  27. package/dist/genai/http.d.ts +49 -0
  28. package/dist/genai/http.d.ts.map +1 -0
  29. package/dist/genai/http.js +795 -0
  30. package/dist/genai/http.js.map +1 -0
  31. package/dist/genai/semconv.d.ts +64 -0
  32. package/dist/genai/semconv.d.ts.map +1 -0
  33. package/dist/genai/semconv.js +52 -0
  34. package/dist/genai/semconv.js.map +1 -0
  35. package/dist/genai/sse.d.ts +26 -0
  36. package/dist/genai/sse.d.ts.map +1 -0
  37. package/dist/genai/sse.js +100 -0
  38. package/dist/genai/sse.js.map +1 -0
  39. package/dist/index.d.ts +4 -1
  40. package/dist/index.d.ts.map +1 -1
  41. package/dist/index.js +1 -0
  42. package/dist/index.js.map +1 -1
  43. package/dist/infra/aws.d.ts +7 -4
  44. package/dist/infra/aws.d.ts.map +1 -1
  45. package/dist/infra/aws.js +50 -9
  46. package/dist/infra/aws.js.map +1 -1
  47. package/dist/infra/lambda.js +1 -1
  48. package/dist/infra/lambda.js.map +1 -1
  49. package/dist/infra/messaging.d.ts +10 -2
  50. package/dist/infra/messaging.d.ts.map +1 -1
  51. package/dist/infra/messaging.js +22 -3
  52. package/dist/infra/messaging.js.map +1 -1
  53. package/dist/next.js +1 -1
  54. package/dist/next.js.map +1 -1
  55. package/dist/normalize.d.ts +58 -8
  56. package/dist/normalize.d.ts.map +1 -1
  57. package/dist/normalize.js +383 -79
  58. package/dist/normalize.js.map +1 -1
  59. package/dist/pg.d.ts +12 -2
  60. package/dist/pg.d.ts.map +1 -1
  61. package/dist/pg.js +464 -213
  62. package/dist/pg.js.map +1 -1
  63. package/dist/tracker.d.ts +36 -5
  64. package/dist/tracker.d.ts.map +1 -1
  65. package/dist/tracker.js +55 -6
  66. package/dist/tracker.js.map +1 -1
  67. package/dist/types.d.ts +27 -4
  68. package/dist/types.d.ts.map +1 -1
  69. package/package.json +6 -1
package/dist/pg.js CHANGED
@@ -1,7 +1,8 @@
1
1
  import { AsyncResource } from 'node:async_hooks';
2
+ import { errorMonitor } from 'node:events';
2
3
  import { performance } from 'node:perf_hooks';
3
4
  import { databaseHost } from './database.js';
4
- import { databaseErrorAttributes, sqlCommandText, sqlSpanFields } from './normalize.js';
5
+ import { analyzeSqlCommands, databaseErrorAttributes, sqlSpanFields } from './normalize.js';
5
6
  import { MIN_POOL_WAIT_MS } from './pool-wait.js';
6
7
  const PATCHED = Symbol.for('fg-tracker.pg.patched');
7
8
  /** Marca usada pelo `stread-track-sdk` ≤ 0.3: evita instrumentar duas vezes quando as duas versões convivem. */
@@ -10,8 +11,13 @@ const CLIENT_PATCHED = Symbol.for('fg-tracker.pg.clientPatched');
10
11
  function extractSql(arg) {
11
12
  if (typeof arg === 'string')
12
13
  return arg;
13
- if (typeof arg === 'object' && arg !== null && typeof arg.text === 'string') {
14
- return arg.text;
14
+ if (typeof arg === 'object' && arg !== null) {
15
+ if (typeof arg.text === 'string')
16
+ return arg.text;
17
+ // `pg-query-stream`: o texto fica no cursor interno.
18
+ const cursor = arg.cursor;
19
+ if (typeof arg.submit === 'function' && typeof cursor?.text === 'string')
20
+ return cursor.text;
15
21
  }
16
22
  return null;
17
23
  }
@@ -33,104 +39,205 @@ function recordDbError(span, err) {
33
39
  for (const [key, value] of Object.entries(databaseErrorAttributes(err)))
34
40
  span.setAttribute(key, value);
35
41
  }
42
+ function isSubmittable(value) {
43
+ return typeof value === 'object' && value !== null && typeof value.submit === 'function';
44
+ }
45
+ /** Callback da aplicação no contexto de quem enviou a query, com o `this` que o `pg` lhe dá. */
46
+ function bindCallback(cb, before) {
47
+ return AsyncResource.bind(function fgTrackerQueryCallback(...values) {
48
+ before(values);
49
+ return Reflect.apply(cb, this, values);
50
+ });
51
+ }
36
52
  /**
37
53
  * Troca o callback da query, venha ele no último argumento (`query(text, values, cb)`) ou no
38
- * objeto de configuração (`query({ text, callback })`, que o `pg` também aceita), pelo devolvido por
39
- * `wrap`. O objeto do usuário não é alterado (vai uma cópia). `false` quando não há callback.
54
+ * objeto de configuração (`query({ text, callback })`, que o `pg` também aceita), pelo que avisa
55
+ * `finish` antes de chamá-lo. O objeto do usuário não é alterado (vai uma cópia). `false` quando não
56
+ * há callback.
40
57
  */
41
- function wrapCallback(args, wrap,
42
- /** `pool.query` ignora `config.callback` (devolve Promise): só o `Client` o usa. */
43
- configCallback) {
58
+ function wrapCallback(args, finish, configCallback) {
59
+ const before = (values) => finish(values[0], values[1]);
44
60
  const last = args[args.length - 1];
45
61
  if (typeof last === 'function') {
46
- args[args.length - 1] = wrap(last);
62
+ args[args.length - 1] = bindCallback(last, before);
47
63
  return true;
48
64
  }
49
65
  const config = args[0];
50
66
  // `query(config)` ou `query(config, values)`: com uma função em outro argumento, ela vence (o `pg` faz igual).
51
67
  const onlyValues = args.length === 1 || (args.length === 2 && typeof args[1] !== 'function');
52
- if (configCallback && onlyValues && config && typeof config === 'object' && typeof config.submit !== 'function') {
68
+ if (configCallback && onlyValues && config && typeof config === 'object') {
53
69
  const callback = config.callback;
54
70
  if (typeof callback === 'function') {
55
- args[0] = { ...config, callback: wrap(callback) };
71
+ args[0] = { ...config, callback: bindCallback(callback, before) };
56
72
  return true;
57
73
  }
58
74
  }
59
75
  return false;
60
76
  }
61
77
  /**
62
- * Executa uma query dentro de um span `db.query` (nome = SQL normalizado; `rows`, fingerprint,
63
- * semconv). Suporta as assinaturas (text), (config), (text, values), (text, cb) e (text, values, cb).
64
- * `onSettled` roda depois da query, com o erro (ou `undefined`) e o resultado.
78
+ * Observa um Submittable até o fim real, sem mudar o que a aplicação vê. Avisa `finish` uma vez, no
79
+ * primeiro destes sinais:
80
+ * - o callback do objeto (`new pg.Query(text, cb)`) ou o passado em `client.query(q, cb)`: no erro, o
81
+ * `pg` chama o callback e não emite `error`;
82
+ * - `handleReadyForQuery`/`handleError`, que o `pg` chama no objeto ativo (o fim no servidor e o
83
+ * erro, inclusive o do cursor sem listener de `error`, que não emite nada);
84
+ * - os eventos `end`, `close` (stream destruído antes do fim) e o erro por `events.errorMonitor`,
85
+ * que é avisado antes dos listeners e não conta como um: sem listener da aplicação, o erro continua
86
+ * sendo lançado como sem o SDK.
87
+ * Tudo o que foi instalado sai no primeiro sinal.
65
88
  */
66
- function runQuery(core, original, args, sql, baseAttributes, parent, onStart, onSettled,
67
- /** Query de um `Client`: o callback pode vir em `config.callback`. */
68
- clientQuery = false) {
69
- const fields = sqlSpanFields(sql);
70
- const span = core.startSpan('db.query', fields.name, { rows: null, ...baseAttributes, ...fields.attributes }, parent);
71
- onStart?.(span);
72
- const settle = (err, result) => {
73
- if (err)
74
- recordDbError(span, err);
75
- else
76
- span.setAttribute('rows', rowCount(result));
77
- span.end();
89
+ function observeSubmittable(q, args, finish) {
90
+ const undo = [];
91
+ const done = (err, result) => {
92
+ for (const step of undo.splice(0)) {
93
+ try {
94
+ step();
95
+ }
96
+ catch {
97
+ /* nada a desfazer */
98
+ }
99
+ }
100
+ finish(err, result);
101
+ };
102
+ const before = (values) => done(values[0], values[1]);
103
+ if (typeof q.callback === 'function') {
104
+ // Fica no objeto: no erro, o `pg` chama o callback depois do `handleError` (o primeiro sinal), e
105
+ // ele precisa rodar no contexto de quem enviou. Chamado de novo, só repassa.
106
+ q.callback = bindCallback(q.callback, before);
107
+ }
108
+ else if (typeof args[1] === 'function') {
109
+ args[1] = bindCallback(args[1], before);
110
+ }
111
+ else if (typeof args[2] === 'function') {
112
+ args[2] = bindCallback(args[2], before);
113
+ }
114
+ const hook = (name, when) => {
115
+ const original = q[name];
116
+ if (typeof original !== 'function')
117
+ return;
118
+ const own = Object.hasOwn(q, name);
119
+ const hooked = function fgTrackerSubmittableHook(...values) {
120
+ if (when === 'before')
121
+ done(values[0], undefined);
122
+ try {
123
+ return Reflect.apply(original, this, values);
124
+ }
125
+ finally {
126
+ if (when === 'after')
127
+ done(undefined, q._results ?? q._result);
128
+ }
129
+ };
130
+ q[name] = hooked;
131
+ undo.push(() => {
132
+ if (q[name] !== hooked)
133
+ return;
134
+ if (own)
135
+ q[name] = original;
136
+ else
137
+ delete q[name];
138
+ });
139
+ };
140
+ hook('handleError', 'before');
141
+ hook('handleReadyForQuery', 'after');
142
+ if (typeof q.once === 'function' && typeof q.removeListener === 'function') {
143
+ const onEnd = (result) => done(undefined, result);
144
+ const onError = (err) => done(err, undefined);
145
+ const onClose = () => done(undefined, undefined);
146
+ q.once('end', onEnd);
147
+ q.once(errorMonitor, onError);
148
+ q.once('close', onClose);
149
+ undo.push(() => {
150
+ q.removeListener('end', onEnd);
151
+ q.removeListener(errorMonitor, onError);
152
+ q.removeListener('close', onClose);
153
+ });
154
+ }
155
+ }
156
+ /**
157
+ * Chama `original(...args)` e avisa `onFinish` uma única vez, quando a query termina de verdade:
158
+ * callback (nos argumentos ou em `config.callback`), Promise, Submittable ({@link observeSubmittable})
159
+ * ou, num client compatível que devolve outra coisa, na hora. `register` guarda a finalização de
160
+ * quem ainda está em voo (a conexão encerrada termina o que sobrar).
161
+ */
162
+ function callTracked(original, args, onFinish,
163
+ /** `pool.query` ignora `config.callback` (devolve Promise): só o `Client` o usa. */
164
+ configCallback, register,
165
+ /**
166
+ * Executa a chamada ao `pg` (o `runQuery` a põe dentro do span da query, para o que o `pg` fizer
167
+ * nela). Os callbacks e a continuação da Promise são ligados antes, fora dela: rodam no contexto de
168
+ * quem enviou a query, nunca no do span da query, que já terá fechado.
169
+ */
170
+ invoke = (call) => call()) {
171
+ let finished = false;
172
+ let unregister;
173
+ const finish = (err, result) => {
174
+ if (finished)
175
+ return;
176
+ finished = true;
177
+ unregister?.();
78
178
  try {
79
- onSettled?.(err, result);
179
+ onFinish(err, result);
80
180
  }
81
181
  catch {
82
- /* acompanhamento de transação nunca quebra a query */
182
+ /* acompanhamento nunca quebra a query */
83
183
  }
84
184
  };
185
+ const submittable = isSubmittable(args[0]) ? args[0] : null;
85
186
  // O pg chama o callback no contexto do socket (o de quem abriu a conexão): volta ao de quem
86
187
  // enviou a query, para a finalização e o código do usuário.
87
- const withCallback = wrapCallback(args, (cb) => AsyncResource.bind((err, result) => {
88
- settle(err, result);
89
- cb(err, result);
90
- }), clientQuery);
91
- if (withCallback)
92
- return core.runInSpan(span, () => original(...args));
188
+ const withCallback = submittable ? false : wrapCallback(args, finish, configCallback);
189
+ if (submittable)
190
+ observeSubmittable(submittable, args, finish);
191
+ if (submittable || withCallback)
192
+ unregister = register?.(finish);
93
193
  let out;
94
194
  try {
95
- out = core.runInSpan(span, () => original(...args));
195
+ out = invoke(() => original(...args));
96
196
  }
97
197
  catch (err) {
98
- settle(err, undefined);
198
+ finish(err, undefined);
99
199
  throw err;
100
200
  }
201
+ if (submittable || withCallback)
202
+ return out;
101
203
  if (out && typeof out.then === 'function') {
102
204
  return out.then((result) => {
103
- settle(undefined, result);
205
+ finish(undefined, result);
104
206
  return result;
105
207
  }, (err) => {
106
- settle(err, undefined);
208
+ finish(err, undefined);
107
209
  throw err;
108
210
  });
109
211
  }
110
- settle(undefined, out);
212
+ finish(undefined, out);
111
213
  return out;
112
214
  }
113
- /** BEGIN/START TRANSACTION abrem; COMMIT/END fecham; ROLLBACK/ABORT (não `ROLLBACK TO`) desfazem. */
114
- function transactionCommand(sql) {
115
- const head = sql.replace(/^\s*(?:--[^\n]*\n\s*|\/\*[\s\S]*?\*\/\s*)*/, '').slice(0, 40).toLowerCase();
116
- if (/^(begin|start\s+transaction)\b/.test(head))
117
- return 'begin';
118
- if (/^(commit|end)\b/.test(head) && !/^commit\s+prepared\b/.test(head))
119
- return 'commit';
120
- if (/^(rollback|abort)\b/.test(head) && !/^rollback\s+(to|prepared)\b/.test(head))
121
- return 'rollback';
122
- return null;
123
- }
124
- /** Comandos do texto (separados por `;`, ignorando literais e comentários) e os de transação, em ordem. */
125
- function sqlStatements(sql) {
126
- const statements = sqlCommandText(sql).split(';').filter((statement) => statement.trim());
127
- const transaction = [];
128
- for (const statement of statements) {
129
- const command = transactionCommand(statement);
130
- if (command)
131
- transaction.push(command);
132
- }
133
- return { count: statements.length, transaction };
215
+ /**
216
+ * Executa uma query dentro de um span `db.query` (nome = SQL normalizado; `rows`, fingerprint,
217
+ * semconv). Suporta as assinaturas (text), (config), (text, values), (text, cb), (text, values, cb)
218
+ * e Submittables; o span fecha no fim real da query.
219
+ */
220
+ function runQuery(core, original, args, sql, baseAttributes, options = {}) {
221
+ const fields = sqlSpanFields(sql, 'postgresql');
222
+ const span = core.startSpan('db.query', fields.name, { rows: null, ...baseAttributes, ...fields.attributes }, options.parent);
223
+ options.onStart?.(span);
224
+ const settle = (err, result) => {
225
+ if (err)
226
+ recordDbError(span, err);
227
+ else
228
+ span.setAttribute('rows', rowCount(result));
229
+ try {
230
+ options.beforeEnd?.(err, result, span);
231
+ }
232
+ catch {
233
+ /* atributos são best-effort */
234
+ }
235
+ span.end();
236
+ options.onSettled?.(err, result);
237
+ };
238
+ // Só a chamada ao `pg` roda no span da query; o callback da aplicação volta ao contexto de quem
239
+ // enviou (a raiz dela, com as decisões dela), como antes da 0.14.
240
+ return callTracked(original, args, settle, options.clientQuery === true, options.register, (call) => core.runInSpan(span, call));
134
241
  }
135
242
  /**
136
243
  * Estado da transação no servidor depois da última resposta (`ReadyForQuery`): `I` fora de
@@ -156,6 +263,17 @@ const SYNTAX_ERROR = '42601';
156
263
  function errorCode(err) {
157
264
  return typeof err === 'object' && err !== null ? err.code : undefined;
158
265
  }
266
+ /**
267
+ * Erro de sintaxe da análise do texto inteiro (nada rodou). O 42601 de um `EXECUTE` dinâmico em
268
+ * PL/pgSQL acontece em tempo de execução, depois do que veio antes, e o servidor o marca com o
269
+ * contexto (`where`) e a query interna (`internalQuery`).
270
+ */
271
+ function isParseError(err) {
272
+ if (errorCode(err) !== SYNTAX_ERROR || typeof err !== 'object' || err === null)
273
+ return false;
274
+ const { where, internalQuery } = err;
275
+ return !where && !internalQuery;
276
+ }
159
277
  /** Texto de um prepared statement executado só pelo nome, como o `pg` o guardou na conexão. */
160
278
  function preparedText(client, config) {
161
279
  const name = config && typeof config === 'object' ? config.name : undefined;
@@ -174,36 +292,18 @@ function answeredCommand(result) {
174
292
  const last = Array.isArray(result) ? result[result.length - 1] : result;
175
293
  return last?.command;
176
294
  }
177
- /**
178
- * Acompanha uma query sem SQL em texto (prepared statement executado só pelo nome,
179
- * `client.query({ name })`): sem span, só para a transação seguir o estado do servidor.
180
- */
181
- function observeQuery(original, args, onSettled) {
182
- const settle = (err, result) => {
183
- try {
184
- onSettled(err, result);
185
- }
186
- catch {
187
- /* acompanhamento nunca quebra a query */
188
- }
189
- };
190
- const withCallback = wrapCallback(args, (cb) => AsyncResource.bind((err, result) => {
191
- settle(err, result);
192
- cb(err, result);
193
- }), true);
194
- if (withCallback)
195
- return original(...args);
196
- const out = original(...args);
197
- if (out && typeof out.then === 'function') {
198
- return out.then((result) => {
199
- settle(undefined, result);
200
- return result;
201
- }, (err) => {
202
- settle(err, undefined);
203
- throw err;
204
- });
205
- }
206
- return out;
295
+ /** Texto desconhecido (nome preparado fora desta conexão): pode encerrar a transação. */
296
+ const UNKNOWN_TEXT = { count: 1, transaction: [], closes: true, ends: true, startsWithBegin: false };
297
+ /** Contexto que a transação aceita como dona: mesma execução e mesmas decisões de amostragem e exclusão. */
298
+ function sameExecution(core, sender, tx) {
299
+ if (core.localRootOfContext(sender) === tx.localRootId)
300
+ return true;
301
+ // Raiz de mensagem aberta pelo `trackMessage` dentro da raiz da transação (lote com BEGIN antes do laço).
302
+ const execution = core.executionRootOfContext(sender);
303
+ return execution !== undefined && execution === tx.localRootId;
304
+ }
305
+ function sameDecisions(sender, tx) {
306
+ return (sender.sampled !== false) === tx.sampled && (sender.ignored === true) === tx.ignored;
207
307
  }
208
308
  function poolStats(pool) {
209
309
  const out = {};
@@ -222,8 +322,18 @@ function poolStats(pool) {
222
322
  * - `pool.connect()`: span `custom` `pg connect` com a espera por conexão (`db.poolWaitMs`) e a
223
323
  * ocupação do pool; o client devolvido também gera spans `db.query`;
224
324
  * - transação num client (`BEGIN` … `COMMIT`/`ROLLBACK`): span `custom` `pg transaction`, pai das
225
- * queries dela, com `db.transaction.outcome` (`commit`, `rollback` ou `released_open` quando o
226
- * client volta ao pool com a transação aberta).
325
+ * queries dela, com `db.transaction.outcome` (`commit`, `rollback`, `prepared` ou `released_open`
326
+ * quando o client volta ao pool com a transação aberta).
327
+ *
328
+ * Quem é pai de uma query num client com transação aberta (CONTRATO.md §16.1):
329
+ * - a mesma execução que abriu a transação (mesma raiz local, ou a raiz de mensagem que o
330
+ * `trackMessage` abriu dentro dela), com as mesmas decisões de amostragem e exclusão: a transação,
331
+ * salvo com um COMMIT/ROLLBACK dela já a caminho;
332
+ * - outra execução (outra requisição, mesmo do mesmo trace distribuído): o próprio contexto, com as
333
+ * próprias decisões; se rodar dentro da transação confirmada, leva `links` para ela e
334
+ * `db.transaction.foreign: true`;
335
+ * - quem envia sem contexto: a transação só na janela confirmada pelo servidor (BEGIN respondido,
336
+ * nenhum comando de encerramento em voo em nenhuma leitura do texto); fora dela, nenhum span.
227
337
  */
228
338
  export function instrumentPgPool(core, pool) {
229
339
  const target = pool;
@@ -241,7 +351,7 @@ export function instrumentPgPool(core, pool) {
241
351
  const stateOf = (client) => {
242
352
  let state = states.get(client);
243
353
  if (!state) {
244
- state = { tx: null, internal: false, pending: 0, seq: 0, txSeq: 0, closingSeq: 0 };
354
+ state = { tx: null, queued: null, internal: false, seq: 0, inFlight: new Map() };
245
355
  states.set(client, state);
246
356
  }
247
357
  return state;
@@ -251,118 +361,198 @@ export function instrumentPgPool(core, pool) {
251
361
  * próprio pool. O span da query recebe a espera (`db.poolWaitMs`) em vez de um span `pg connect`.
252
362
  */
253
363
  let poolQuery = null;
254
- const endTransaction = (state, outcome, err) => {
364
+ const finishSpan = (tx, outcome, err) => {
365
+ tx.span.setAttribute('db.transaction.outcome', outcome);
366
+ if (err)
367
+ recordDbError(tx.span, err);
368
+ tx.span.end();
369
+ };
370
+ /**
371
+ * Fecha a transação atual. Pelo fim observado no servidor (`promote`), o BEGIN que esperava na fila
372
+ * passa a ser a transação atual (ainda sem confirmação); no release e na conexão encerrada, ele
373
+ * fecha junto.
374
+ */
375
+ const endTransaction = (state, outcome, err, promote = false) => {
255
376
  const tx = state.tx;
256
- if (!tx)
377
+ if (tx) {
378
+ state.tx = null;
379
+ finishSpan(tx, outcome, err);
380
+ }
381
+ const queued = state.queued;
382
+ if (!queued)
257
383
  return;
258
- state.tx = null;
259
- state.closingSeq = 0;
260
- tx.setAttribute('db.transaction.outcome', outcome);
261
- if (err)
262
- recordDbError(tx, err);
263
- tx.end();
384
+ state.queued = null;
385
+ if (promote)
386
+ state.tx = queued;
387
+ else
388
+ finishSpan(queued, outcome);
264
389
  };
265
390
  /**
266
- * Fecha ou abre o span da transação depois de uma query. Fonte primária: o estado que o servidor
267
- * devolve no `ReadyForQuery` (`getTransactionStatus()`, nas versões do `pg` que o têm). Plano B, para drivers sem o
268
- * método e para erros (o callback de erro do `pg` roda antes do `ReadyForQuery`, com o estado
269
- * anterior): todos os comandos de transação do texto, em ordem.
391
+ * Comando de encerramento de `tx` ainda sem fim real: enviado depois do BEGIN dela ou no mesmo
392
+ * envio (`BEGIN; …; COMMIT`).
270
393
  */
271
- const settleTransaction = (client, state, sql, err, result,
272
- /** Abre o span da transação no contexto de quem enviou a query; `null` fora de um trace. */
394
+ const closingAfter = (state, tx, which) => {
395
+ if (!tx)
396
+ return false;
397
+ for (const [seq, query] of state.inFlight)
398
+ if (seq >= tx.seq && query[which])
399
+ return true;
400
+ return false;
401
+ };
402
+ const closingInFlight = (state, which) => closingAfter(state, state.tx, which);
403
+ /**
404
+ * Fecha, abre ou confirma a transação depois do fim real de uma query. Fonte primária: o estado
405
+ * que o servidor devolve no `ReadyForQuery` (`getTransactionStatus()` ou o evento da conexão). Sem
406
+ * estado nenhum (client compatível), os comandos de transação do texto, em ordem.
407
+ */
408
+ const settleTransaction = (client, state, analysis, err, result,
409
+ /** Abre o span da transação no contexto de quem enviou a query; `null` sem contexto. */
273
410
  startTx,
274
411
  /** O span da transação foi aberto por esta query (o texto começa com BEGIN). */
275
412
  openedHere,
276
- /** Ordem de envio desta query (`state.seq` no envio). */
277
- seq) => {
278
- // Query enviada antes do BEGIN que abriu a transação atual (fila do `pg`): o estado que a
279
- // resposta dela traz é de antes da transação e não a fecha.
280
- if (state.tx && seq < state.txSeq)
281
- return;
282
- const open = (start) => {
283
- state.tx = start();
284
- state.txSeq = seq;
285
- };
286
- const outcomeFor = (last) => {
287
- if (err)
288
- return last === 'commit' ? 'commit_failed' : last === 'begin' ? 'begin_failed' : 'rollback';
413
+ /** Ordem de envio desta query. */
414
+ seq,
415
+ /** Fim real: a query sai da lista de em voo (no erro antes do `ReadyForQuery`, só quando ele chega). */
416
+ release) => {
417
+ const failed = err !== undefined && err !== null;
418
+ const last = analysis.transaction[analysis.transaction.length - 1] ?? null;
419
+ const outcomeFor = () => {
420
+ if (failed)
421
+ return last === 'commit' ? 'commit_failed' : last === 'begin' ? 'begin_failed' : last === 'prepare' ? 'prepare_failed' : 'rollback';
289
422
  const answered = answeredCommand(result);
290
- return answered === 'ROLLBACK' || (answered !== 'COMMIT' && last === 'rollback') ? 'rollback' : 'commit';
423
+ if (answered === 'ROLLBACK')
424
+ return 'rollback';
425
+ if (last === 'prepare')
426
+ return 'prepared';
427
+ return answered !== 'COMMIT' && last === 'rollback' ? 'rollback' : 'commit';
428
+ };
429
+ const fromServer = (status) => {
430
+ // Query enviada antes do BEGIN que abriu a transação atual (fila do `pg`): o estado que a
431
+ // resposta dela traz é de antes da transação e não a fecha.
432
+ if (state.tx && seq < state.tx.seq)
433
+ return;
434
+ if (status === 'I') {
435
+ if (state.tx)
436
+ endTransaction(state, outcomeFor(), err, true);
437
+ }
438
+ else if (state.tx) {
439
+ state.tx.confirmed = true;
440
+ }
441
+ else if (startTx) {
442
+ // `SELECT 1; BEGIN`, BEGIN preparado: a transação existe a partir desta resposta.
443
+ state.tx = { span: startTx(), seq, confirmed: true };
444
+ }
291
445
  };
292
- const statements = sqlStatements(sql);
293
- const last = statements.transaction[statements.transaction.length - 1] ?? null;
294
446
  const status = serverTransactionStatus(client, state.observedStatus);
295
447
  // Erro com o `ReadyForQuery` ainda por chegar: o estado do servidor é o de antes da query.
296
- const stale = err !== undefined && err !== null && client.readyForQuery === false;
448
+ const stale = failed && client.readyForQuery === false;
297
449
  const connection = client.connection;
298
- const canAwaitServer = stale && typeof connection?.once === 'function';
299
- if (status !== undefined && !stale) {
300
- if (status === 'I' && state.tx)
301
- endTransaction(state, outcomeFor(last), err);
302
- else if (status !== 'I' && !state.tx && startTx)
303
- open(startTx); // `SELECT 1; BEGIN`, BEGIN preparado
304
- return;
305
- }
306
- if (statements.transaction.length > 0) {
307
- if (!err) {
308
- // Sem erro, todos os comandos rodaram: vale o efeito do último.
309
- if (last === 'begin') {
310
- if (!state.tx && startTx)
311
- open(startTx);
450
+ if (stale && (state.tx === null || seq >= state.tx.seq)) {
451
+ // O próprio erro já diz o estado em dois casos, sem esperar a resposta (a próxima query de quem
452
+ // recebeu o erro sai antes dela): o texto recusado na análise não rodou nada, e o único comando,
453
+ // um COMMIT/ROLLBACK/PREPARE TRANSACTION que falhou, tirou a sessão da transação.
454
+ if (isParseError(err)) {
455
+ release();
456
+ if (openedHere && state.tx === openedHere)
457
+ endTransaction(state, 'begin_failed', err, true);
458
+ else if (openedHere && state.queued === openedHere) {
459
+ state.queued = null;
460
+ finishSpan(openedHere, 'begin_failed', err);
312
461
  }
313
- else if (state.tx) {
314
- endTransaction(state, outcomeFor(last), err);
315
- }
316
- }
317
- else if (errorCode(err) === SYNTAX_ERROR) {
318
- // Erro de sintaxe: o Postgres analisa o texto inteiro antes de executar, então nenhum
319
- // comando rodou e o estado do servidor é o de antes. Só a transação que esta query abriu
320
- // (o BEGIN dela) deixa de existir.
321
- if (openedHere && state.tx)
322
- endTransaction(state, 'begin_failed', err);
323
- }
324
- else if (last === 'commit' || last === 'rollback') {
325
- // COMMIT que falha termina a transação; com vários comandos não se sabe qual falhou, e
326
- // fechar é o lado seguro (mantida aberta, a próxima requisição herdaria o trace).
327
- if (state.tx)
328
- endTransaction(state, outcomeFor(last), err);
462
+ return;
329
463
  }
330
- else if (state.tx && statements.count === 1) {
331
- endTransaction(state, 'begin_failed', err); // só `BEGIN`, e ele falhou
464
+ if (analysis.count === 1 && (last === 'commit' || last === 'rollback' || last === 'prepare') && state.tx) {
465
+ release();
466
+ endTransaction(state, outcomeFor(), err, true);
467
+ return;
332
468
  }
333
- // Outro erro num texto que termina com BEGIN: o BEGIN rodou e o comando seguinte falhou, então a
334
- // transação está abortada (`E`) e continua aberta até o ROLLBACK.
335
469
  }
336
- // Erro com o servidor ainda por responder: confere o estado quando o `ReadyForQuery` chegar
337
- // (o estado observado é atualizado na frente de qualquer listener).
338
- if (canAwaitServer) {
339
- const current = state.tx;
470
+ if (stale && typeof connection?.once === 'function') {
471
+ // A decisão espera o estado do servidor (L2): `SELECT 1/0; COMMIT` deixa a transação abortada
472
+ // aberta, e um 42601 do EXECUTE dinâmico não quer dizer que o BEGIN não rodou. Até lá a query
473
+ // continua em voo (protege a janela de quem vem depois).
340
474
  try {
341
475
  connection.once('readyForQuery', () => {
342
- // Só a transação desta query: um BEGIN enviado depois já abriu outra.
343
- if (!current || state.tx !== current)
344
- return;
476
+ release();
345
477
  const now = serverTransactionStatus(client, state.observedStatus);
346
- if (now === 'I')
347
- endTransaction(state, outcomeFor(last), err);
478
+ if (now !== undefined)
479
+ fromServer(now);
480
+ else if (openedHere && state.tx === openedHere)
481
+ endTransaction(state, 'begin_failed', err, true);
348
482
  });
483
+ return;
349
484
  }
350
485
  catch {
351
- /* sem o evento, fica o que o texto decidiu */
486
+ /* sem o evento, decide agora pelo texto */
352
487
  }
353
488
  }
489
+ release();
490
+ if (openedHere && state.queued === openedHere) {
491
+ // O BEGIN da fila respondeu antes de a transação anterior fechar: rodou dentro dela e não abriu
492
+ // outra (o servidor só avisa). A transação da fila fecha sem ter existido.
493
+ state.queued = null;
494
+ finishSpan(openedHere, 'begin_failed', failed ? err : undefined);
495
+ }
496
+ if (status !== undefined && !stale) {
497
+ fromServer(status);
498
+ return;
499
+ }
500
+ if (state.tx && seq < state.tx.seq)
501
+ return;
502
+ if (!failed) {
503
+ // Sem erro, todos os comandos rodaram: vale o efeito do último.
504
+ if (last === 'begin' || last === 'chain') {
505
+ if (state.tx)
506
+ state.tx.confirmed = true;
507
+ else if (startTx)
508
+ state.tx = { span: startTx(), seq, confirmed: true };
509
+ }
510
+ else if (last && state.tx) {
511
+ endTransaction(state, outcomeFor(), err, true);
512
+ }
513
+ else if (state.tx && openedHere === state.tx) {
514
+ state.tx.confirmed = true;
515
+ }
516
+ return;
517
+ }
518
+ if (!state.tx || analysis.transaction.length === 0)
519
+ return;
520
+ if (errorCode(err) === SYNTAX_ERROR) {
521
+ // Erro de sintaxe: o Postgres analisa o texto inteiro antes de executar, então nenhum
522
+ // comando rodou e o estado do servidor é o de antes. Só a transação que esta query abriu
523
+ // (o BEGIN dela) deixa de existir.
524
+ if (openedHere && state.tx === openedHere)
525
+ endTransaction(state, 'begin_failed', err, true);
526
+ }
527
+ else if (last === 'commit' || last === 'rollback' || last === 'prepare') {
528
+ // COMMIT que falha termina a transação; com vários comandos não se sabe qual falhou, e
529
+ // fechar é o lado seguro (mantida aberta, a próxima requisição herdaria o trace).
530
+ endTransaction(state, outcomeFor(), err, true);
531
+ }
532
+ else if (analysis.count === 1 && last === 'begin') {
533
+ endTransaction(state, 'begin_failed', err, true); // só `BEGIN`, e ele falhou
534
+ }
535
+ // Outro erro num texto que termina com BEGIN: o BEGIN rodou e o comando seguinte falhou, então a
536
+ // transação está abortada (`E`) e continua aberta até o ROLLBACK (sem confirmação do servidor).
354
537
  };
355
538
  const patchClient = (client) => {
356
539
  if (!client || typeof client.query !== 'function' || client[CLIENT_PATCHED])
357
540
  return;
358
541
  client[CLIENT_PATCHED] = true;
359
542
  // Conexão encerrada (`client.end()`, queda, término pelo servidor): o Postgres desfaz a
360
- // transação aberta; o span fecha junto, senão ficaria retido no tracker para sempre.
543
+ // transação aberta; o span fecha junto, senão ficaria retido no tracker para sempre. O que ainda
544
+ // estava em voo (cursor nunca fechado) termina aqui.
361
545
  if (typeof client.once === 'function') {
362
546
  try {
363
547
  client.once('end', () => {
364
548
  const state = states.get(client);
365
- if (state?.tx)
549
+ if (!state)
550
+ return;
551
+ const closed = new Error('Connection terminated');
552
+ for (const query of [...state.inFlight.values()])
553
+ query.finish?.(closed, undefined);
554
+ state.inFlight.clear();
555
+ if (state.tx || state.queued)
366
556
  endTransaction(state, 'connection_closed');
367
557
  });
368
558
  }
@@ -370,9 +560,9 @@ export function instrumentPgPool(core, pool) {
370
560
  /* sem o evento, a transação fecha pela próxima query ou pelo release */
371
561
  }
372
562
  }
563
+ const connection = client.connection;
373
564
  // Estado do servidor em cada `ReadyForQuery`, na frente do listener do próprio client (que
374
565
  // resolve a query): vale para as versões do `pg` sem `getTransactionStatus()`.
375
- const connection = client.connection;
376
566
  if (typeof connection?.prependListener === 'function') {
377
567
  try {
378
568
  connection.prependListener('readyForQuery', (msg) => {
@@ -385,6 +575,20 @@ export function instrumentPgPool(core, pool) {
385
575
  /* sem o evento, fica o texto */
386
576
  }
387
577
  }
578
+ // `standard_conforming_strings` da sessão (início da conexão e cada SET): decide a leitura
579
+ // principal do texto. Com barra invertida no texto, as duas leituras protegem a janela.
580
+ if (typeof connection?.on === 'function') {
581
+ try {
582
+ connection.on('parameterStatus', (msg) => {
583
+ const { parameterName, parameterValue } = (msg ?? {});
584
+ if (parameterName === 'standard_conforming_strings')
585
+ stateOf(client).standardConformingStrings = parameterValue === 'off' ? 'off' : 'on';
586
+ });
587
+ }
588
+ catch {
589
+ /* sem o evento, as duas leituras */
590
+ }
591
+ }
388
592
  const clientQuery = client.query.bind(client);
389
593
  client.query = function fgTrackerClientQuery(...args) {
390
594
  const state = stateOf(client);
@@ -392,7 +596,7 @@ export function instrumentPgPool(core, pool) {
392
596
  return clientQuery(...args);
393
597
  // Rede de segurança: sem query em voo e com o servidor fora de transação, uma transação que
394
598
  // ainda conste aberta terminou por um caminho não observado; não pode virar pai desta query.
395
- if (state.tx && state.pending === 0 && client.readyForQuery !== false && serverTransactionStatus(client, state.observedStatus) === 'I') {
599
+ if (state.tx && state.inFlight.size === 0 && client.readyForQuery !== false && serverTransactionStatus(client, state.observedStatus) === 'I') {
396
600
  endTransaction(state, 'unobserved_end');
397
601
  }
398
602
  // Prepared statement executado só pelo nome: o texto é o que o `pg` guardou ao prepará-lo nesta
@@ -401,46 +605,91 @@ export function instrumentPgPool(core, pool) {
401
605
  // O contexto de quem envia a query: o callback do pg roda no contexto do socket, que pode ser
402
606
  // o da requisição que abriu a conexão.
403
607
  const sender = core.currentContext();
608
+ if (!state.tx && !state.queued && !sender)
609
+ return clientQuery(...args);
610
+ // Análise dos comandos de transação feita uma vez por query, para o envio e para a resposta (O1).
611
+ const analysis = sql === null ? UNKNOWN_TEXT : analyzeSqlCommands(sql, state.standardConformingStrings);
404
612
  const startTx = sender ? () => core.startSpan('custom', 'pg transaction', { ...baseAttributes, 'db.operation.name': 'transaction' }, sender) : null;
405
- if (!sql) {
406
- // Sem texto conhecido (client compatível, nome preparado fora desta conexão): sem span, mas um
407
- // BEGIN, COMMIT ou ROLLBACK preparado abre ou termina a transação pelo estado do servidor.
408
- if (!state.tx && !startTx)
409
- return clientQuery(...args);
410
- const seq = ++state.seq;
411
- state.pending += 1;
412
- return observeQuery(clientQuery, args, (err, result) => {
413
- state.pending = Math.max(0, state.pending - 1);
414
- settleTransaction(client, state, '', err, result, startTx, false, seq);
415
- });
416
- }
417
- const command = transactionCommand(sql);
418
613
  const seq = ++state.seq;
419
- const openedHere = command === 'begin' && !state.tx && !!startTx;
420
- if (openedHere) {
421
- state.tx = startTx();
422
- state.txSeq = seq;
423
- }
424
- // Com um COMMIT/ROLLBACK da transação já a caminho, esta query roda depois dele, fora dela.
425
- const tx = state.tx && state.closingSeq === 0 ? state.tx : null;
426
- const parent = tx ? tx.context() : core.currentContext();
427
- if (!parent)
428
- return clientQuery(...args);
429
- // Inclusive no texto que abre e fecha a transação no mesmo envio (`BEGIN; …; COMMIT`).
430
- if (tx) {
431
- const statements = sqlStatements(sql);
432
- const last = statements.transaction[statements.transaction.length - 1];
433
- if (last === 'commit' || last === 'rollback')
434
- state.closingSeq = seq;
435
- }
436
- state.pending += 1;
437
- return runQuery(core, clientQuery, args, sql, baseAttributes, parent, undefined, (err, result) => {
438
- state.pending = Math.max(0, state.pending - 1);
439
- settleTransaction(client, state, sql, err, result, startTx, openedHere, seq);
440
- // Respondido sem fechar a transação (erro de sintaxe, por exemplo): ela continua sendo o pai.
441
- if (state.closingSeq === seq)
442
- state.closingSeq = 0;
443
- }, true);
614
+ const tx = state.tx;
615
+ let parent;
616
+ let foreign = null;
617
+ const queued = state.queued;
618
+ if (queued && sender && sameExecution(core, sender, queued.span) && sameDecisions(sender, queued.span)) {
619
+ // Atrás do BEGIN que esperava na fila: roda na transação que ele vai abrir, salvo com um
620
+ // COMMIT/ROLLBACK dela já a caminho (roda depois dele, fora dela).
621
+ parent = closingAfter(state, queued, 'ends') ? sender : queued.span.context();
622
+ }
623
+ else if (tx && sender && sameExecution(core, sender, tx.span) && sameDecisions(sender, tx.span)) {
624
+ // Com um COMMIT/ROLLBACK da transação já a caminho, esta query roda depois dele, fora dela.
625
+ parent = closingInFlight(state, 'ends') ? sender : tx.span.context();
626
+ }
627
+ else if (tx && tx.confirmed && !closingInFlight(state, 'closes')) {
628
+ // Dentro da janela confirmada pelo servidor: quem envia sem contexto roda na transação e fica
629
+ // sob ela; outra execução fica sob o próprio contexto, com o vínculo.
630
+ if (sender) {
631
+ parent = sender;
632
+ foreign = tx;
633
+ }
634
+ else {
635
+ parent = tx.span.context();
636
+ }
637
+ }
638
+ else {
639
+ // Outra execução, ou ninguém, fora da janela (BEGIN sem resposta, encerramento em voo,
640
+ // dúvida): o próprio contexto; sem contexto, nenhum span (como antes do #38).
641
+ parent = sender;
642
+ }
643
+ let openedHere = null;
644
+ if (analysis.startsWithBegin && startTx && (!state.tx || (!state.queued && closingInFlight(state, 'ends')))) {
645
+ openedHere = { span: startTx(), seq, confirmed: false };
646
+ if (state.tx)
647
+ state.queued = openedHere;
648
+ else
649
+ state.tx = openedHere;
650
+ parent = openedHere.span.context();
651
+ }
652
+ const flight = { closes: analysis.closes, ends: analysis.ends };
653
+ state.inFlight.set(seq, flight);
654
+ const register = (finish) => {
655
+ flight.finish = finish;
656
+ return () => {
657
+ flight.finish = undefined;
658
+ };
659
+ };
660
+ let released = false;
661
+ const release = () => {
662
+ if (released)
663
+ return;
664
+ released = true;
665
+ state.inFlight.delete(seq);
666
+ };
667
+ const settle = (err, result) => settleTransaction(client, state, analysis, err, result, startTx, openedHere, seq, release);
668
+ if (!sql || !parent) {
669
+ // Sem texto conhecido ou sem pai: sem span, mas a transação segue o estado do servidor.
670
+ return callTracked(clientQuery, args, settle, true, register);
671
+ }
672
+ return runQuery(core, clientQuery, args, sql, baseAttributes, {
673
+ parent,
674
+ clientQuery: true,
675
+ register,
676
+ onSettled: settle,
677
+ onStart: (span) => {
678
+ if (!foreign)
679
+ return;
680
+ span.setAttribute('links', [{ traceId: foreign.span.traceId, spanId: foreign.span.spanId }]);
681
+ span.setAttribute('db.transaction.foreign', true);
682
+ },
683
+ beforeEnd: (err, _result, span) => {
684
+ // A resposta mostra o servidor fora de transação: a query não rodou na de outra execução.
685
+ if (!foreign || (err !== undefined && err !== null) || analysis.transaction.length > 0)
686
+ return;
687
+ if (serverTransactionStatus(client, state.observedStatus) !== 'I')
688
+ return;
689
+ delete span.attributes.links;
690
+ delete span.attributes['db.transaction.foreign'];
691
+ },
692
+ });
444
693
  };
445
694
  };
446
695
  /** Envolve o `release` desta retirada (o pool troca a função a cada `connect`). */
@@ -450,7 +699,7 @@ export function instrumentPgPool(core, pool) {
450
699
  return;
451
700
  client.release = function fgTrackerRelease(...args) {
452
701
  state.internal = false;
453
- if (state.tx)
702
+ if (state.tx || state.queued)
454
703
  endTransaction(state, 'released_open', args[0] instanceof Error ? args[0] : undefined);
455
704
  client.release = release;
456
705
  return Reflect.apply(release, this, args);
@@ -471,8 +720,10 @@ export function instrumentPgPool(core, pool) {
471
720
  try {
472
721
  if (!tracked)
473
722
  return original(...args);
474
- return runQuery(core, original, args, sql, baseAttributes, undefined, (span) => {
475
- marker.span = span;
723
+ return runQuery(core, original, args, sql, baseAttributes, {
724
+ onStart: (span) => {
725
+ marker.span = span;
726
+ },
476
727
  });
477
728
  }
478
729
  finally {