@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 +35 -0
- package/cookbook/09-otel-wired.ts +155 -0
- package/cookbook/README.md +1 -0
- package/dist/index.cjs +21 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +21 -2
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
+
}
|
package/cookbook/README.md
CHANGED
|
@@ -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
|
-
|
|
275
|
-
|
|
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) {
|