@nzila/sdk 0.1.0 → 0.1.3
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/README.md +27 -1
- package/dist/cli/constants.d.ts +13 -0
- package/dist/cli/constants.d.ts.map +1 -0
- package/dist/cli/constants.js +20 -0
- package/dist/cli/main.d.ts +3 -0
- package/dist/cli/main.d.ts.map +1 -0
- package/dist/cli/main.js +223 -0
- package/dist/cli/open-browser.d.ts +2 -0
- package/dist/cli/open-browser.d.ts.map +1 -0
- package/dist/cli/open-browser.js +26 -0
- package/examples/COMPLEX-USAGE.md +113 -0
- package/examples/INDEX.md +27 -0
- package/examples/README.md +94 -0
- package/examples/RUNTIME-TRACE.md +222 -0
- package/examples/TRACING-FAILURES.md +186 -0
- package/examples/backend/error-tracing.test.ts +44 -0
- package/examples/backend/order-pricing.test.ts +54 -0
- package/examples/backend/order-pricing.ts +27 -0
- package/examples/backend/runtime-trace.example.ts +31 -0
- package/examples/backend/sample-payload.json +62 -0
- package/examples/backend/sample-tracing-payload.json +90 -0
- package/examples/complex/api-services.demo.ts +72 -0
- package/examples/complex/full-platform.demo.test.ts +46 -0
- package/examples/frontend/checkout-form.test.ts +54 -0
- package/examples/frontend/checkout-form.ts +29 -0
- package/examples/frontend/error-tracing.test.ts +59 -0
- package/examples/frontend/sample-payload.json +62 -0
- package/examples/frontend/sample-tracing-payload.json +73 -0
- package/examples/frontend-webhook-client.ts +32 -0
- package/examples/jest.config.example.cjs +18 -0
- package/examples/mocharc.nzila.example.cjs +21 -0
- package/examples/react/checkout-feature.example.tsx +41 -0
- package/examples/send-run-manual.mjs +16 -0
- package/examples/shared/feature-map.ts +24 -0
- package/examples/shared/load-env.mjs +24 -0
- package/examples/shared/send-webhook.mjs +43 -0
- package/examples/vitest.config.example.ts +20 -0
- package/package.json +9 -4
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
# Runtime feature tracing (`traceNzilaFeature` / `useNzilaFeature`)
|
|
2
|
+
|
|
3
|
+
Pair **Vitest** failures (webhook) with **live** signals on the same **feature** name (e.g. `Checkout`). Call one function at the top of the UI component or backend handler that owns that feature; use **`captureError`** when you already handle errors.
|
|
4
|
+
|
|
5
|
+
## SDK package exports
|
|
6
|
+
|
|
7
|
+
| Import path | Use for |
|
|
8
|
+
|-------------|---------|
|
|
9
|
+
| `@nzila/sdk` | `initNzilaRuntime`, `traceNzilaFeature`, `reportFeatureError`, Vitest/Jest reporters |
|
|
10
|
+
| `@nzila/sdk/react` | `useNzilaFeature`, `NzilaFeatureBoundary`, `reportFeatureError` |
|
|
11
|
+
| `@nzila/sdk/runtime` | Low-level runtime client only |
|
|
12
|
+
| `@nzila/sdk/vitest` | `NzilaVitestReporter` |
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Setup (once per app)
|
|
17
|
+
|
|
18
|
+
```ts
|
|
19
|
+
import { initNzilaRuntime } from "@nzila/sdk/runtime";
|
|
20
|
+
// or: import { initNzilaRuntime } from "@nzila/sdk";
|
|
21
|
+
|
|
22
|
+
initNzilaRuntime({
|
|
23
|
+
signalsUrl: process.env.NZILA_SIGNALS_URL!, // https://your-app.vercel.app/api/signals/runtime
|
|
24
|
+
apiKey: process.env.NZILA_API_KEY!,
|
|
25
|
+
locale: "en",
|
|
26
|
+
renderLoopThreshold: 40, // optional — React render-loop detection
|
|
27
|
+
});
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
| Variable | Where |
|
|
31
|
+
|----------|--------|
|
|
32
|
+
| `NZILA_SIGNALS_URL` | Node / Route Handlers |
|
|
33
|
+
| `NEXT_PUBLIC_NZILA_SIGNALS_URL` | Browser (Next.js) |
|
|
34
|
+
| `NZILA_API_KEY` / `NEXT_PUBLIC_NZILA_API_KEY` | Same project key as test webhooks |
|
|
35
|
+
|
|
36
|
+
Requires DB migration `0003_feature_intelligence.sql` (`npm run db:migrate`).
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## API reference
|
|
41
|
+
|
|
42
|
+
| Function | Signature | When to use |
|
|
43
|
+
|----------|-----------|-------------|
|
|
44
|
+
| `initNzilaRuntime` | `(config)` once | Before any trace calls |
|
|
45
|
+
| `useNzilaFeature` | `(feature, options?)` | **Top of React component** |
|
|
46
|
+
| `traceNzilaFeature` | `(feature)` | **Top of backend handler** |
|
|
47
|
+
| `handle.captureError` | `(error, detail?)` | Your `try/catch`, toasts, form errors |
|
|
48
|
+
| `handle.captureApiError` | `(error, label, detail?)` | Caught fetch/HTTP without `runApi` |
|
|
49
|
+
| `handle.runApi` | `(label, fn)` | Wrap async API calls |
|
|
50
|
+
| `handle.reportError` | same as `captureError` | Alias |
|
|
51
|
+
| `reportFeatureError` | `(feature, error, detail?)` | Shared util; pass feature + error |
|
|
52
|
+
| `captureFeatureError` | alias of `reportFeatureError` | |
|
|
53
|
+
| `extractErrorFields` | `(error)` | `message`, `name`, `stack` normalization |
|
|
54
|
+
|
|
55
|
+
Returned **handle** (from hook or `traceNzilaFeature`): `{ feature, runApi, captureError, captureApiError, reportError, reportUi }`.
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## React — `useNzilaFeature` at the top of the component
|
|
60
|
+
|
|
61
|
+
```tsx
|
|
62
|
+
"use client";
|
|
63
|
+
|
|
64
|
+
import { useNzilaFeature } from "@nzila/sdk/react";
|
|
65
|
+
import { initNzilaRuntime } from "@nzila/sdk/react";
|
|
66
|
+
|
|
67
|
+
// e.g. app/layout.tsx or instrumentation.ts (once)
|
|
68
|
+
initNzilaRuntime({
|
|
69
|
+
signalsUrl: process.env.NEXT_PUBLIC_NZILA_SIGNALS_URL!,
|
|
70
|
+
apiKey: process.env.NEXT_PUBLIC_NZILA_API_KEY!,
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
export function CheckoutPage() {
|
|
74
|
+
const nzila = useNzilaFeature("Checkout"); // same feature as Vitest mapFeature
|
|
75
|
+
|
|
76
|
+
async function pay() {
|
|
77
|
+
await nzila.runApi("POST /api/checkout", () =>
|
|
78
|
+
fetch("/api/checkout", { method: "POST", body: "{}" }),
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return <button onClick={() => void pay()}>Pay</button>;
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Manual error handling (your `try/catch`)
|
|
87
|
+
|
|
88
|
+
Use the handle from the hook (feature is already bound):
|
|
89
|
+
|
|
90
|
+
```tsx
|
|
91
|
+
export function CheckoutPage() {
|
|
92
|
+
const nzila = useNzilaFeature("Checkout");
|
|
93
|
+
|
|
94
|
+
async function pay() {
|
|
95
|
+
try {
|
|
96
|
+
await nzila.runApi("POST /api/checkout", () =>
|
|
97
|
+
fetch("/api/checkout", { method: "POST" }),
|
|
98
|
+
);
|
|
99
|
+
} catch (err) {
|
|
100
|
+
nzila.captureError(err, { step: "pay", handled: true });
|
|
101
|
+
// your toast / form error state
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Or pass **feature + error** from anywhere (e.g. shared error util):
|
|
108
|
+
|
|
109
|
+
```ts
|
|
110
|
+
import { reportFeatureError } from "@nzila/sdk/react";
|
|
111
|
+
|
|
112
|
+
reportFeatureError("Checkout", error, { step: "validation" });
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
`captureError` uses `extractErrorFields` — works with `Error`, strings, and unknown values.
|
|
116
|
+
|
|
117
|
+
### What is reported automatically
|
|
118
|
+
|
|
119
|
+
| Issue | Signal `type` | `payload` notes |
|
|
120
|
+
|-------|---------------|-----------------|
|
|
121
|
+
| Too many re-renders in ~1s | `ui` | `kind: "render_loop"`, `renderCount` |
|
|
122
|
+
| `window.onerror` | `error` | while component mounted |
|
|
123
|
+
| Unhandled promise rejection | `error` | while mounted |
|
|
124
|
+
| `runApi` failure | `api` | `success: false`, message |
|
|
125
|
+
| `runApi` success | `api` | `success: true`, `label` |
|
|
126
|
+
|
|
127
|
+
Optional error boundary:
|
|
128
|
+
|
|
129
|
+
```tsx
|
|
130
|
+
import { NzilaFeatureBoundary } from "@nzila/sdk/react";
|
|
131
|
+
|
|
132
|
+
<NzilaFeatureBoundary feature="Checkout" nzila={nzila}>
|
|
133
|
+
<CheckoutForm />
|
|
134
|
+
</NzilaFeatureBoundary>
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
## Backend — `traceNzilaFeature` at the top of the handler
|
|
140
|
+
|
|
141
|
+
```ts
|
|
142
|
+
import { initNzilaRuntime, traceNzilaFeature } from "@nzila/sdk";
|
|
143
|
+
|
|
144
|
+
initNzilaRuntime({
|
|
145
|
+
signalsUrl: process.env.NZILA_SIGNALS_URL!,
|
|
146
|
+
apiKey: process.env.NZILA_API_KEY!,
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
export async function postCheckout(req: Request) {
|
|
150
|
+
const nzila = traceNzilaFeature("Checkout");
|
|
151
|
+
|
|
152
|
+
try {
|
|
153
|
+
const upstream = await nzila.runApi("payment-gateway", () =>
|
|
154
|
+
fetch("https://api.example.com/charge", { method: "POST" }),
|
|
155
|
+
);
|
|
156
|
+
if (!upstream.ok) throw new Error(`charge failed: ${upstream.status}`);
|
|
157
|
+
return Response.json({ ok: true });
|
|
158
|
+
} catch (err) {
|
|
159
|
+
nzila.captureError(err, { route: "POST /checkout", handled: true });
|
|
160
|
+
return Response.json({ error: "payment_failed" }, { status: 502 });
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Caught an API error without `runApi`?
|
|
166
|
+
|
|
167
|
+
```ts
|
|
168
|
+
nzila.captureApiError(err, "payment-gateway", { status: 502 });
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
From a nested helper without the handle:
|
|
172
|
+
|
|
173
|
+
```ts
|
|
174
|
+
import { reportFeatureError } from "@nzila/sdk";
|
|
175
|
+
|
|
176
|
+
reportFeatureError("Checkout", err, { layer: "inventory-service" });
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
Call **`traceNzilaFeature("Checkout")` as the first line** of the route/service that matches your tested feature.
|
|
180
|
+
|
|
181
|
+
Repo example: [backend/runtime-trace.example.ts](./backend/runtime-trace.example.ts).
|
|
182
|
+
|
|
183
|
+
---
|
|
184
|
+
|
|
185
|
+
## Dashboard
|
|
186
|
+
|
|
187
|
+
Open **Feature health** for the project — compare **tests** (from webhooks) with **runtime** (`api` / `ui` / `error` signals).
|
|
188
|
+
|
|
189
|
+
Use the **same feature string** everywhere:
|
|
190
|
+
|
|
191
|
+
| Source | Example |
|
|
192
|
+
|--------|---------|
|
|
193
|
+
| Vitest `describe` / `mapFeature` | `"Checkout"` |
|
|
194
|
+
| `useNzilaFeature("…")` | `"Checkout"` |
|
|
195
|
+
| `traceNzilaFeature("…")` | `"Checkout"` |
|
|
196
|
+
| `reportFeatureError("…", err)` | `"Checkout"` |
|
|
197
|
+
| Manual HTTP (advanced) | `POST /api/signals/runtime` |
|
|
198
|
+
|
|
199
|
+
Test-side tracing: [TRACING-FAILURES.md](./TRACING-FAILURES.md).
|
|
200
|
+
Deploy (Vercel + Render): [../docs/DEPLOY-VERCEL-RENDER.md](../docs/DEPLOY-VERCEL-RENDER.md).
|
|
201
|
+
|
|
202
|
+
---
|
|
203
|
+
|
|
204
|
+
## Manual HTTP (optional)
|
|
205
|
+
|
|
206
|
+
Same payload the SDK sends:
|
|
207
|
+
|
|
208
|
+
```json
|
|
209
|
+
{
|
|
210
|
+
"locale": "en",
|
|
211
|
+
"signals": [
|
|
212
|
+
{
|
|
213
|
+
"feature": "Checkout",
|
|
214
|
+
"type": "error",
|
|
215
|
+
"success": false,
|
|
216
|
+
"payload": { "message": "card declined", "handled": true }
|
|
217
|
+
}
|
|
218
|
+
]
|
|
219
|
+
}
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
Types: `navigation`, `api`, `ui`, `error`, `custom`.
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
# Tracing test failures in Nzila
|
|
2
|
+
|
|
3
|
+
How failures move from **backend** and **frontend** test runs into the dashboard, how to **attach tests to a feature**, and how to trace an error even when the test was already in your suite.
|
|
4
|
+
|
|
5
|
+
## Mental model
|
|
6
|
+
|
|
7
|
+
```text
|
|
8
|
+
describe() tree → describePath + fingerprint
|
|
9
|
+
mapFeature / feature field → dashboard "Feature" column
|
|
10
|
+
test name → human label
|
|
11
|
+
errors[] → processed into Failure analysis (not raw stacks in UI)
|
|
12
|
+
worker → summaries + feature health
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
1. Vitest finishes one run → reporter sends **one** webhook (`runId` is idempotent).
|
|
16
|
+
2. Each test becomes a row with `describePath`, `feature`, `testName`, `status`, `errors`.
|
|
17
|
+
3. The worker enriches failures and updates **Stability**, **Failures**, and **Feature health**.
|
|
18
|
+
|
|
19
|
+
## Developer setup (one-time)
|
|
20
|
+
|
|
21
|
+
### 1. Run Nzila locally
|
|
22
|
+
|
|
23
|
+
From the repo root (see [README](../README.md)):
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npm install
|
|
27
|
+
cp .env.example .env.local # fill DATABASE_URL, AUTH_SECRET, etc.
|
|
28
|
+
npm run db:migrate
|
|
29
|
+
npm run dev # http://localhost:3000
|
|
30
|
+
npm run worker # second terminal — processes webhooks
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### 2. Create two projects (API + web)
|
|
34
|
+
|
|
35
|
+
In the dashboard, create **two** projects so backend and frontend keys do not mix:
|
|
36
|
+
|
|
37
|
+
| Project display name | **App name** (must match Vitest `appName`) | Example tests |
|
|
38
|
+
|----------------------|------------------------------------------|---------------|
|
|
39
|
+
| Checkout API | `checkout-api` | `examples/backend/` |
|
|
40
|
+
| Checkout Web | `checkout-web` | `examples/frontend/` |
|
|
41
|
+
|
|
42
|
+
For each project: **Settings → API keys** → create a key. You can use one key for both demos if both projects belong to your org and you switch `NZILA_API_KEY` when running each suite — separate keys are clearer.
|
|
43
|
+
|
|
44
|
+
### 3. Configure `.env.local`
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
NZILA_WEBHOOK_URL=http://localhost:3000/api/webhooks/tests
|
|
48
|
+
NZILA_API_KEY=nzl_your_key_for_checkout_api
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
For frontend tracing, use the **checkout-web** key (or change the key before `example:test:tracing:frontend`).
|
|
52
|
+
|
|
53
|
+
### 4. Run tracing demos
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
npm run example:test:tracing:backend # failures → checkout-api
|
|
57
|
+
npm run example:test:tracing:frontend # failures → checkout-web
|
|
58
|
+
npm run example:test:tracing:all # both
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Tracing-only Vitest configs (reporter + `mapFeature`):
|
|
62
|
+
|
|
63
|
+
- `examples/vitest.tracing.backend.config.ts`
|
|
64
|
+
- `examples/vitest.tracing.frontend.config.ts`
|
|
65
|
+
|
|
66
|
+
## Attach tests to a feature
|
|
67
|
+
|
|
68
|
+
Nzila groups failures and health by **feature** string. You have three ways to set it:
|
|
69
|
+
|
|
70
|
+
### A. Top-level `describe` name (default)
|
|
71
|
+
|
|
72
|
+
The Vitest reporter uses the **first** `describe()` segment as `feature` unless you override with `mapFeature`.
|
|
73
|
+
|
|
74
|
+
```ts
|
|
75
|
+
describe("Checkout", () => {
|
|
76
|
+
it("should reject expired cards", () => { /* ... */ });
|
|
77
|
+
});
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
→ `feature: "Checkout"`, fingerprint includes full `describePath`.
|
|
81
|
+
|
|
82
|
+
### B. `mapFeature` in the reporter (recommended for UI vs product names)
|
|
83
|
+
|
|
84
|
+
When suite titles are long (e.g. `"Checkout UI"`) but the product feature is `"Checkout"`, map them in config:
|
|
85
|
+
|
|
86
|
+
```ts
|
|
87
|
+
// examples/shared/feature-map.ts
|
|
88
|
+
export function resolveFrontendFeature(describePath: string[], _testName: string) {
|
|
89
|
+
if (describePath[0] === "Checkout UI") return "Checkout";
|
|
90
|
+
return describePath[0] ?? "general";
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Wire it in `NzilaVitestReporter({ mapFeature: resolveFrontendFeature, ... })`.
|
|
95
|
+
|
|
96
|
+
### C. Explicit `feature` in webhook JSON
|
|
97
|
+
|
|
98
|
+
For manual/CI payloads, set `feature` on each test (see `examples/backend/sample-tracing-payload.json` and `examples/frontend/sample-tracing-payload.json`):
|
|
99
|
+
|
|
100
|
+
```json
|
|
101
|
+
{
|
|
102
|
+
"describePath": ["Checkout UI", "email field"],
|
|
103
|
+
"feature": "Checkout",
|
|
104
|
+
"testName": "should reject malformed email addresses",
|
|
105
|
+
"status": "failed",
|
|
106
|
+
"errors": [{ "message": "...", "stack": "..." }]
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Server fallback: `feature ?? describePath[0] ?? "general"` (`ensureTestCase`).
|
|
111
|
+
|
|
112
|
+
**Runtime signals** should use the **same** feature string so **Feature health** lines up tests and production.
|
|
113
|
+
|
|
114
|
+
**Recommended (live app):** use the SDK so production matches tests:
|
|
115
|
+
|
|
116
|
+
| Layer | Call at top of unit | Manual `catch` |
|
|
117
|
+
|-------|---------------------|----------------|
|
|
118
|
+
| React | `const nzila = useNzilaFeature("Checkout")` | `nzila.captureError(err, { … })` |
|
|
119
|
+
| Backend | `const nzila = traceNzilaFeature("Checkout")` | `nzila.captureError(err, { … })` |
|
|
120
|
+
| Either | — | `reportFeatureError("Checkout", err, { … })` |
|
|
121
|
+
|
|
122
|
+
Auto-reported: render loops (`ui`), `runApi` failures (`api`), unhandled window errors. Full API: **[RUNTIME-TRACE.md](./RUNTIME-TRACE.md)**.
|
|
123
|
+
|
|
124
|
+
## Example suites
|
|
125
|
+
|
|
126
|
+
| Layer | File | App name | Script |
|
|
127
|
+
|-------|------|----------|--------|
|
|
128
|
+
| Backend | `examples/backend/error-tracing.test.ts` | `checkout-api` | `npm run example:test:tracing:backend` |
|
|
129
|
+
| Frontend | `examples/frontend/error-tracing.test.ts` | `checkout-web` | `npm run example:test:tracing:frontend` |
|
|
130
|
+
|
|
131
|
+
Failures are intentional so you can practice tracing without breaking production code paths.
|
|
132
|
+
|
|
133
|
+
## Trace a failure after tests ran (even with tests in CI)
|
|
134
|
+
|
|
135
|
+
Tests passing or failing does not block tracing: each **run** is stored with per-test `errors`. When a test **fails** (or starts failing in a new run):
|
|
136
|
+
|
|
137
|
+
1. **Test runs** (`/en/runs`) — open the latest run for `checkout-api` or `checkout-web`.
|
|
138
|
+
2. **Failure analysis** (`/en/failures`) — read processed summaries (from `errors[].message`, not raw stacks).
|
|
139
|
+
3. **Feature insights** / **Feature health** — filter by the same `feature` you attached in the suite or payload.
|
|
140
|
+
4. **Code** — match **fingerprint**: `describePath[0] › … › testName` (same `describe` + `it` names as in repo).
|
|
141
|
+
|
|
142
|
+
Optional: send runtime signals for the same feature and compare on **Feature health**.
|
|
143
|
+
|
|
144
|
+
## Manual webhook (no Vitest)
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
npm run example:webhook:tracing # backend sample
|
|
148
|
+
npm run example:webhook:tracing:frontend # frontend sample
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Requires `NZILA_API_KEY` and `NZILA_WEBHOOK_URL` in `.env.local` (script loads env via `send-webhook.mjs`).
|
|
152
|
+
|
|
153
|
+
## Webhook error fields
|
|
154
|
+
|
|
155
|
+
Per test in `tests[]`:
|
|
156
|
+
|
|
157
|
+
```json
|
|
158
|
+
{
|
|
159
|
+
"describePath": ["Checkout", "Payment"],
|
|
160
|
+
"feature": "Checkout",
|
|
161
|
+
"testName": "should reject expired cards",
|
|
162
|
+
"status": "failed",
|
|
163
|
+
"duration": 42,
|
|
164
|
+
"errors": [
|
|
165
|
+
{ "message": "expected false to be true", "stack": "AssertionError: ..." }
|
|
166
|
+
]
|
|
167
|
+
}
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
- **message** — drives operator-facing copy after processing.
|
|
171
|
+
- **stack** — optional; used server-side, not shown as-is on **Failures**.
|
|
172
|
+
|
|
173
|
+
## Related files
|
|
174
|
+
|
|
175
|
+
| File | Role |
|
|
176
|
+
|------|------|
|
|
177
|
+
| `examples/shared/feature-map.ts` | Backend/frontend feature mapping |
|
|
178
|
+
| `@nzila/sdk/vitest-reporter.ts` | Vitest → webhook + `mapFeature` |
|
|
179
|
+
| `src/lib/webhook-schema.ts` | Payload validation |
|
|
180
|
+
| `src/server/test-cases.ts` | `feature` resolution |
|
|
181
|
+
| `src/server/process-run.ts` | Worker enrichment |
|
|
182
|
+
| `examples/TRACING-FAILURES.md` | This guide (tests) |
|
|
183
|
+
| `examples/RUNTIME-TRACE.md` | Live `useNzilaFeature`, `captureError` |
|
|
184
|
+
| `examples/backend/runtime-trace.example.ts` | Backend handler sample |
|
|
185
|
+
| `@nzila/sdk/runtime/` | Runtime client |
|
|
186
|
+
| `@nzila/sdk/react/` | React hook + boundary |
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Demo suite for tracing failures in Nzila.
|
|
3
|
+
* Top-level describe() names become "feature" in the webhook and dashboard.
|
|
4
|
+
*
|
|
5
|
+
* Run: npm run example:test:tracing
|
|
6
|
+
*/
|
|
7
|
+
import { describe, it, expect } from "vitest";
|
|
8
|
+
|
|
9
|
+
describe("Checkout", () => {
|
|
10
|
+
describe("Payment", () => {
|
|
11
|
+
it("should accept valid card payloads", () => {
|
|
12
|
+
expect({ status: "authorized" }).toEqual({ status: "authorized" });
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("should reject expired cards", () => {
|
|
16
|
+
const cardExpired = false;
|
|
17
|
+
expect(cardExpired).toBe(true);
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
describe("Coupons", () => {
|
|
22
|
+
it("should apply percentage discounts", () => {
|
|
23
|
+
const total = 100;
|
|
24
|
+
const discounted = total * 0.9;
|
|
25
|
+
expect(discounted).toBe(80);
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
describe("Inventory", () => {
|
|
31
|
+
it("should block checkout when stock is zero", () => {
|
|
32
|
+
const available = 0;
|
|
33
|
+
const canCheckout = available > 0;
|
|
34
|
+
expect(canCheckout).toBe(true);
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe("Auth API", () => {
|
|
39
|
+
it("should rate-limit repeated login failures", () => {
|
|
40
|
+
const attempts = 3;
|
|
41
|
+
const locked = attempts >= 5;
|
|
42
|
+
expect(locked).toBe(true);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { applyCoupon, lineTotal, orderTotal, reserveInventory } from "./order-pricing";
|
|
3
|
+
|
|
4
|
+
describe("Payment API", () => {
|
|
5
|
+
describe("order pricing", () => {
|
|
6
|
+
it("should compute line totals from quantity and unit price", () => {
|
|
7
|
+
expect(lineTotal({ sku: "SKU-1", qty: 2, unitCents: 1500 })).toBe(3000);
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it("should apply SAVE10 coupon to subtotal", () => {
|
|
11
|
+
expect(applyCoupon(10_000, "save10")).toBe(9000);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("should reject negative quantities", () => {
|
|
15
|
+
expect(() => lineTotal({ sku: "SKU-2", qty: -1, unitCents: 100 })).toThrow();
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
describe("checkout total", () => {
|
|
20
|
+
it("should sum multiple line items before coupon", () => {
|
|
21
|
+
const total = orderTotal(
|
|
22
|
+
[
|
|
23
|
+
{ sku: "A", qty: 1, unitCents: 2500 },
|
|
24
|
+
{ sku: "B", qty: 2, unitCents: 500 },
|
|
25
|
+
],
|
|
26
|
+
"FREESHIP",
|
|
27
|
+
);
|
|
28
|
+
expect(total).toBe(3500);
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
describe("Inventory service", () => {
|
|
34
|
+
it("should reserve stock when quantity is available", () => {
|
|
35
|
+
expect(reserveInventory("widget", 3, 10)).toBe(true);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("should fail reservation when stock is insufficient", () => {
|
|
39
|
+
expect(reserveInventory("widget", 50, 10)).toBe(false);
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
describe("Auth API", () => {
|
|
44
|
+
it("should reject invalid password attempts", () => {
|
|
45
|
+
const attempts = 6;
|
|
46
|
+
const maxAttempts = 5;
|
|
47
|
+
expect(attempts > maxAttempts).toBe(true);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("should lock account after 5 failures", () => {
|
|
51
|
+
const accountLocked = false;
|
|
52
|
+
expect(accountLocked).toBe(true);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export type LineItem = { sku: string; qty: number; unitCents: number };
|
|
2
|
+
|
|
3
|
+
export function lineTotal(item: LineItem): number {
|
|
4
|
+
if (item.qty < 0 || item.unitCents < 0) {
|
|
5
|
+
throw new Error("Invalid line item");
|
|
6
|
+
}
|
|
7
|
+
return item.qty * item.unitCents;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function applyCoupon(subtotalCents: number, code: string): number {
|
|
11
|
+
if (subtotalCents < 0) throw new Error("Invalid subtotal");
|
|
12
|
+
const normalized = code.trim().toUpperCase();
|
|
13
|
+
if (normalized === "SAVE10") return Math.round(subtotalCents * 0.9);
|
|
14
|
+
if (normalized === "FREESHIP") return subtotalCents;
|
|
15
|
+
return subtotalCents;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function orderTotal(items: LineItem[], coupon?: string): number {
|
|
19
|
+
const subtotal = items.reduce((sum, item) => sum + lineTotal(item), 0);
|
|
20
|
+
return coupon ? applyCoupon(subtotal, coupon) : subtotal;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function reserveInventory(sku: string, qty: number, onHand: number): boolean {
|
|
24
|
+
if (!sku.trim()) return false;
|
|
25
|
+
if (qty <= 0) return false;
|
|
26
|
+
return onHand >= qty;
|
|
27
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Backend pattern: call traceNzilaFeature at the top of the handler
|
|
3
|
+
* that owns the same feature name as your Vitest suite.
|
|
4
|
+
*
|
|
5
|
+
* Env: NZILA_SIGNALS_URL, NZILA_API_KEY
|
|
6
|
+
*/
|
|
7
|
+
import { initNzilaRuntime, traceNzilaFeature } from "@nzila/sdk/runtime";
|
|
8
|
+
|
|
9
|
+
initNzilaRuntime({
|
|
10
|
+
signalsUrl:
|
|
11
|
+
process.env.NZILA_SIGNALS_URL ??
|
|
12
|
+
"http://localhost:3000/api/signals/runtime",
|
|
13
|
+
apiKey: process.env.NZILA_API_KEY ?? "",
|
|
14
|
+
locale: "en",
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
export async function exampleCheckoutHandler(): Promise<{ ok: boolean }> {
|
|
18
|
+
const nzila = traceNzilaFeature("Checkout");
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
await nzila.runApi("inventory-reserve", async () => {
|
|
22
|
+
const res = await fetch("https://httpbin.org/status/500");
|
|
23
|
+
if (!res.ok) throw new Error(`inventory ${res.status}`);
|
|
24
|
+
return res;
|
|
25
|
+
});
|
|
26
|
+
return { ok: true };
|
|
27
|
+
} catch (error) {
|
|
28
|
+
nzila.captureError(error, { handler: "exampleCheckoutHandler", handled: true });
|
|
29
|
+
return { ok: false };
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
{
|
|
2
|
+
"runId": "backend-run-placeholder",
|
|
3
|
+
"appName": "checkout-api",
|
|
4
|
+
"framework": "vitest",
|
|
5
|
+
"startedAt": "2026-05-16T14:00:00.000Z",
|
|
6
|
+
"finishedAt": "2026-05-16T14:00:04.200Z",
|
|
7
|
+
"tests": [
|
|
8
|
+
{
|
|
9
|
+
"appName": "checkout-api",
|
|
10
|
+
"framework": "vitest",
|
|
11
|
+
"startedAt": "2026-05-16T14:00:00.000Z",
|
|
12
|
+
"finishedAt": "2026-05-16T14:00:00.800Z",
|
|
13
|
+
"describePath": ["Payment API", "order pricing"],
|
|
14
|
+
"feature": "Payment API",
|
|
15
|
+
"testName": "should compute line totals from quantity and unit price",
|
|
16
|
+
"status": "passed",
|
|
17
|
+
"duration": 12,
|
|
18
|
+
"errors": []
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"appName": "checkout-api",
|
|
22
|
+
"framework": "vitest",
|
|
23
|
+
"startedAt": "2026-05-16T14:00:00.800Z",
|
|
24
|
+
"finishedAt": "2026-05-16T14:00:01.400Z",
|
|
25
|
+
"describePath": ["Payment API", "order pricing"],
|
|
26
|
+
"feature": "Payment API",
|
|
27
|
+
"testName": "should apply SAVE10 coupon to subtotal",
|
|
28
|
+
"status": "passed",
|
|
29
|
+
"duration": 8,
|
|
30
|
+
"errors": []
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"appName": "checkout-api",
|
|
34
|
+
"framework": "vitest",
|
|
35
|
+
"startedAt": "2026-05-16T14:00:01.400Z",
|
|
36
|
+
"finishedAt": "2026-05-16T14:00:02.000Z",
|
|
37
|
+
"describePath": ["Inventory service"],
|
|
38
|
+
"feature": "Inventory",
|
|
39
|
+
"testName": "should reserve stock when quantity is available",
|
|
40
|
+
"status": "passed",
|
|
41
|
+
"duration": 15,
|
|
42
|
+
"errors": []
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"appName": "checkout-api",
|
|
46
|
+
"framework": "vitest",
|
|
47
|
+
"startedAt": "2026-05-16T14:00:02.000Z",
|
|
48
|
+
"finishedAt": "2026-05-16T14:00:03.200Z",
|
|
49
|
+
"describePath": ["Auth API"],
|
|
50
|
+
"feature": "Auth API",
|
|
51
|
+
"testName": "should lock account after 5 failures",
|
|
52
|
+
"status": "failed",
|
|
53
|
+
"duration": 95,
|
|
54
|
+
"errors": [
|
|
55
|
+
{
|
|
56
|
+
"message": "Expected accountLocked to be true, received false",
|
|
57
|
+
"stack": "AssertionError: expected false to be true\n at examples/backend/order-pricing.test.ts"
|
|
58
|
+
}
|
|
59
|
+
]
|
|
60
|
+
}
|
|
61
|
+
]
|
|
62
|
+
}
|