@ar-agents/mercadopago 0.11.0 → 0.12.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/CHANGELOG.md CHANGED
@@ -1,5 +1,40 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.12.0
4
+
5
+ ### Minor Changes — Idempotency-by-default for state-mutating writes
6
+
7
+ `MercadoPagoClient` now auto-generates a UUID v4 X-Idempotency-Key header on
8
+ every state-mutating POST request when the caller doesn't provide one
9
+ explicitly. Naive callers (and the LLM tools layer) often forget to pass an
10
+ idempotency key, leaving them exposed to double-charge bugs on network
11
+ partitions. This makes the safe default: safe.
12
+
13
+ - **Auto-generated keys are unique per call** (Web Crypto's `randomUUID()` —
14
+ Edge Runtime + Node 19+ + Cloudflare Workers + browsers).
15
+ - **Caller-supplied keys still win** — pass `idempotencyKey: "..."` for
16
+ deterministic retries from a job queue (e.g., same key across retry
17
+ attempts).
18
+ - **Only POST requests are auto-keyed.** GET / DELETE are HTTP-idempotent
19
+ by spec. PUT skips auto-gen because MP's PUT endpoints encode the dedup
20
+ key in the resource path (`/v1/payments/:id` → cancel; `/preapproval/:id` →
21
+ pause/resume — already deduped by id).
22
+
23
+ 6 new tests in `idempotency-default.test.ts` verify:
24
+ - UUID v4 format on auto-gen
25
+ - Different keys per call
26
+ - Caller-supplied keys honored over auto-gen
27
+ - GET requests NOT keyed
28
+ - Works for `createPayment`, `createPreference`, `createPreapproval`
29
+
30
+ ### New cookbook recipe
31
+
32
+ - `cookbook/09-otel-wired.ts` — full OpenTelemetry wiring example. Shows
33
+ how to wire `traceContext` for distributed-trace correlation, instrument
34
+ the client + tools, and what the resulting trace + metric shape looks
35
+ like in your APM. Closes the half-finished OTel story (lib + subpath
36
+ existed since v0.10 but no recipe wired it end-to-end).
37
+
3
38
  ## 0.11.0
4
39
 
5
40
  ### Minor Changes — Composability + cross-LATAM + fraud scoring
@@ -0,0 +1,155 @@
1
+ /**
2
+ * Recipe 09 — OpenTelemetry wired end-to-end.
3
+ *
4
+ * # Pattern
5
+ *
6
+ * 1. Wire an OTel SDK at app boot (NodeSDK or Edge equivalent)
7
+ * 2. Build the MP client with `traceContext` so each MP request injects a
8
+ * W3C `traceparent` header (correlates with your distributed traces)
9
+ * 3. Wrap the client / tools with the OTel instrumentation from
10
+ * `@ar-agents/mercadopago/otel` to get spans + metrics for every call:
11
+ * - `mp.request` span per MP API call (with attrs for endpoint, method,
12
+ * status, duration, retry count)
13
+ * - `mp.tool` span per agent-invoked tool (with input + output attrs)
14
+ * - Metrics: latency p50/p95/p99, error rate, rate-limit-remaining
15
+ * 4. Ship traces + metrics to your OTel collector (Honeycomb, Datadog,
16
+ * New Relic, Grafana Tempo, ...)
17
+ *
18
+ * # Why this matters
19
+ *
20
+ * MP's API is the slow path of any agent that uses it (200-600ms per call).
21
+ * Without observability you can't tell:
22
+ * - Which tool calls are slow (`create_payment` vs `create_subscription` vs `get_payment`)
23
+ * - When MP is degraded (rate-limit-remaining trending down before failures)
24
+ * - Where retries kick in (so you can size your timeout budget correctly)
25
+ *
26
+ * # Setup (one-time at app boot)
27
+ *
28
+ * ```ts
29
+ * // instrumentation.ts (Vercel auto-loads this if present)
30
+ * import { NodeSDK } from "@opentelemetry/sdk-node";
31
+ * import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
32
+ * import { OTLPMetricExporter } from "@opentelemetry/exporter-metrics-otlp-http";
33
+ * import { PeriodicExportingMetricReader } from "@opentelemetry/sdk-metrics";
34
+ *
35
+ * const sdk = new NodeSDK({
36
+ * serviceName: "my-ar-agent",
37
+ * traceExporter: new OTLPTraceExporter({ url: process.env.OTEL_EXPORTER_OTLP_ENDPOINT! }),
38
+ * metricReader: new PeriodicExportingMetricReader({
39
+ * exporter: new OTLPMetricExporter({ url: process.env.OTEL_EXPORTER_OTLP_ENDPOINT! }),
40
+ * exportIntervalMillis: 30_000,
41
+ * }),
42
+ * });
43
+ * sdk.start();
44
+ * ```
45
+ *
46
+ * # Edge Runtime
47
+ *
48
+ * Edge-compatible. Use `@vercel/otel` instead of `@opentelemetry/sdk-node`.
49
+ * The instrumentation in `@ar-agents/mercadopago/otel` is runtime-agnostic
50
+ * (uses the `@opentelemetry/api` interface, no Node-only deps).
51
+ */
52
+
53
+ import { trace, context as otelContext } from "@opentelemetry/api";
54
+ import { Experimental_Agent as Agent, stepCountIs } from "ai";
55
+ import {
56
+ MercadoPagoClient,
57
+ mercadoPagoTools,
58
+ InMemoryStateAdapter,
59
+ CircuitBreaker,
60
+ } from "@ar-agents/mercadopago";
61
+ import {
62
+ instrumentMercadoPagoClient,
63
+ instrumentMercadoPagoTools,
64
+ } from "@ar-agents/mercadopago/otel";
65
+
66
+ const tracer = trace.getTracer("my-ar-agent");
67
+ const meter = trace.getTracer("my-ar-agent"); // simplified — use `metrics.getMeter` in real code
68
+
69
+ // 1. Build the client with traceContext so each MP request injects traceparent.
70
+ // The function returns whatever active OTel context exists at call time.
71
+ const mp = new MercadoPagoClient({
72
+ accessToken: process.env.MP_ACCESS_TOKEN!,
73
+ // Wire OTel context propagation. MP logs will be correlated with your trace
74
+ // graph, and downstream tooling (Datadog APM, Honeycomb) can join the dots.
75
+ traceContext: () => {
76
+ const span = trace.getActiveSpan();
77
+ if (!span) return undefined;
78
+ const ctx = span.spanContext();
79
+ return {
80
+ traceId: ctx.traceId,
81
+ spanId: ctx.spanId,
82
+ traceFlags: ctx.traceFlags,
83
+ };
84
+ },
85
+ // Production hardening — circuit breaker observed by OTel as well.
86
+ circuitBreaker: new CircuitBreaker({
87
+ failureThreshold: 5,
88
+ rollingWindowMs: 60_000,
89
+ cooldownMs: 30_000,
90
+ }),
91
+ maxRetries: 2,
92
+ });
93
+
94
+ // 2. Instrument the client. Wraps every public method with a span + metric.
95
+ const instrumentedMp = instrumentMercadoPagoClient(mp, { tracer, meter });
96
+
97
+ // 3. Build tools as usual, then wrap with OTel tool instrumentation.
98
+ const baseTools = mercadoPagoTools(instrumentedMp, {
99
+ state: new InMemoryStateAdapter(),
100
+ backUrl: "https://example.com/done",
101
+ });
102
+ const tools = instrumentMercadoPagoTools(baseTools, { tracer });
103
+
104
+ // 4. Use the agent. Every tool call becomes a span; every MP request is a
105
+ // nested span with full request context. Open Honeycomb / Tempo and you
106
+ // see the full picture.
107
+ export const agent = new Agent({
108
+ model: "anthropic/claude-sonnet-4-6",
109
+ instructions: "You are a billing assistant for a SaaS in Argentina.",
110
+ tools,
111
+ stopWhen: stepCountIs(8),
112
+ });
113
+
114
+ /**
115
+ * Trace shape (in your APM):
116
+ *
117
+ * tool: agent.generate (root span)
118
+ * ├── tool: create_payment_preference (mp.tool)
119
+ * │ └── http: POST /checkout/preferences (mp.request)
120
+ * │ attrs:
121
+ * │ mp.method = POST
122
+ * │ mp.path = /checkout/preferences
123
+ * │ mp.status = 201
124
+ * │ mp.duration_ms = 287
125
+ * │ mp.retried = false
126
+ * │ mp.idempotency_key = <uuid>
127
+ * │
128
+ * └── tool: get_payment (mp.tool)
129
+ * └── http: GET /v1/payments/9999 (mp.request)
130
+ * attrs:
131
+ * mp.method = GET
132
+ * mp.duration_ms = 142
133
+ *
134
+ * Metrics emitted (with attributes [endpoint, method, status_class]):
135
+ *
136
+ * mp_request_duration_ms (histogram)
137
+ * mp_request_total (counter)
138
+ * mp_request_error_total (counter, by status_class=4xx|5xx|network)
139
+ * mp_circuit_breaker_state (gauge: closed=0, open=1, half_open=0.5)
140
+ * mp_rate_limit_remaining (gauge, from x-ratelimit-remaining response header)
141
+ */
142
+
143
+ // Example: wrap a request handler with a parent span so MP calls join the trace.
144
+ export async function handleAgentRequest(userMessage: string) {
145
+ return tracer.startActiveSpan("agent.request", async (span) => {
146
+ try {
147
+ const result = await agent.generate({ prompt: userMessage });
148
+ span.setAttribute("agent.steps", result.steps.length);
149
+ span.setAttribute("agent.finish_reason", result.finishReason);
150
+ return result;
151
+ } finally {
152
+ span.end();
153
+ }
154
+ });
155
+ }
@@ -16,6 +16,7 @@ deploy on Vercel as-is.
16
16
  | 06 | `06-3ds-challenge.ts` | Detect challenge → redirect buyer → recover via webhook |
17
17
  | 07 | `07-auth-only-order.ts` | `Order` with manual capture → capture later when service completes |
18
18
  | 08 | `08-recovery-patterns.ts` | Retry expired subscriptions, recover stuck-pending payments, etc. |
19
+ | 09 | `09-otel-wired.ts` | Full OpenTelemetry wiring — spans + metrics for every MP call + tool |
19
20
 
20
21
  ## Conventions
21
22
 
package/dist/index.cjs CHANGED
@@ -228,6 +228,23 @@ var DEFAULT_BASE_URL = "https://api.mercadopago.com";
228
228
  function sleep(ms) {
229
229
  return new Promise((resolve) => setTimeout(resolve, ms));
230
230
  }
231
+ function randomUuid() {
232
+ const c = globalThis.crypto;
233
+ if (c?.randomUUID) {
234
+ return c.randomUUID();
235
+ }
236
+ if (c?.getRandomValues) {
237
+ const bytes = new Uint8Array(16);
238
+ c.getRandomValues(bytes);
239
+ bytes[6] = bytes[6] & 15 | 64;
240
+ bytes[8] = bytes[8] & 63 | 128;
241
+ const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
242
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
243
+ }
244
+ throw new Error(
245
+ "MercadoPagoClient: no Web Crypto available for idempotency key generation. This shouldn't happen on Node 19+, Edge Runtime, or modern browsers."
246
+ );
247
+ }
231
248
  var MercadoPagoClient = class {
232
249
  accessToken;
233
250
  baseUrl;
@@ -271,8 +288,10 @@ var MercadoPagoClient = class {
271
288
  Authorization: `Bearer ${this.accessToken}`,
272
289
  "Content-Type": "application/json"
273
290
  };
274
- if (options?.idempotencyKey) {
275
- headers["X-Idempotency-Key"] = options.idempotencyKey;
291
+ const isMutatingPost = method === "POST";
292
+ const idempotencyKey = options?.idempotencyKey ?? (isMutatingPost ? randomUuid() : void 0);
293
+ if (idempotencyKey) {
294
+ headers["X-Idempotency-Key"] = idempotencyKey;
276
295
  }
277
296
  const trace = this.traceContext?.();
278
297
  if (trace?.traceId && trace?.spanId) {