Resolution
How resourceType, action, and resourceId are resolved from HTTP requests, ORM operations, and message queues.
Every interaction needs a resourceType, action, and resourceId. The auto-instrumentation resolves each one through a chain — first match wins:
resolve()function — full override / escape hatch- Route map / model map / topic map — declarative config
- Convention-based inference — defaults
- Fallback — generic type
Convention defaults
With zero configuration, the auto-instrumentation infers everything from the request:
| Source | resourceType | action |
|---|---|---|
POST /api/orders | ORDER | CREATE |
GET /api/users/123 | USER | READ |
PUT /api/orders/456 | ORDER | UPDATE |
DELETE /api/items/789 | ITEM | DELETE |
PATCH /api/settings | SETTING | UPDATE |
PUT /api/orders/1/items/2 | ITEM | UPDATE |
POST /api/users/1/posts | POST | CREATE |
The conventions are:
- resourceType — extracted from the last meaningful URL segment, singularized, and converted to
UPPER_SNAKE_CASE./api/orders→ORDER,/api/orders/123/items/456→ITEM - action — mapped from the HTTP method:
GET→READ,POST→CREATE,PUT→UPDATE,PATCH→UPDATE,DELETE→DELETE - resourceId — extracted from the last route param (
req.params.itemId), response body (result.id), or request body (body.id)
Segments that look like IDs (UUIDs, numbers) or common prefixes (api, v1, v2) are skipped automatically.
Nested routes
Nested routes like /api/orders/:orderId/items/:itemId are handled automatically:
- resourceType resolves to the leaf resource —
ITEM, notORDER - resourceId resolves to the last param —
itemId, notorderId - parent context is extracted and included in the interaction metadata
PUT /api/orders/ord-1/items/item-2
→ resourceType: ITEM
→ action: UPDATE
→ resourceId: item-2
→ interaction.parent: { resourceType: "ORDER", resourceId: "ord-1" }This works for any nesting depth:
DELETE /api/orders/1/items/2/variants/3
→ resourceType: VARIANT
→ action: DELETE
→ resourceId: 3
→ interaction.parent: { resourceType: "ORDER", resourceId: "1" }The parent is always the top-level resource in the path. For deeply nested routes, intermediate resources are not tracked as separate parents — use the route map if you need more granular control.
If you need to override the nested convention, the route map takes priority:
instrument({
// ...
routes: {
'POST /api/orders/:orderId/items/:itemId/return': {
resourceType: 'RETURN',
action: 'CREATE',
},
},
resourceId: {
RETURN: (ctx) => ctx.params.itemId,
},
});Route map
When your domain actions don't match HTTP verbs, use the route map:
instrument({
apiKey: process.env.PROVENANCE_API_KEY,
origin: 'order-service',
routes: {
'POST /api/orders': { resourceType: 'ORDER', action: 'PLACE' },
'POST /api/orders/:id/cancel': { resourceType: 'ORDER', action: 'CANCEL' },
'POST /api/orders/:id/ship': { resourceType: 'ORDER', action: 'SHIP' },
'POST /api/orders/:id/refund': { resourceType: 'ORDER', action: 'REFUND' },
'GET /api/orders/:id/invoice': { resourceType: 'INVOICE', action: 'GENERATE' },
},
});Without the route map, POST /api/orders/:id/refund would resolve to resourceType: ORDER, action: CREATE (because POST). The override gives you the domain-specific REFUND action.
Routes not in the map still use convention defaults — you only need to override the ones that don't fit.
Model map
Override how ORM model names map to resource types:
instrument({
// ...
models: {
'User': { resourceType: 'CUSTOMER' }, // Prisma model → your domain type
'OrderLineItem': { resourceType: 'ORDER_LINE' },
'AuditLog': null, // null = don't track this model
},
});Without the model map, prisma.user.create() would resolve to resourceType: USER. The override maps it to CUSTOMER.
Action map
Override the default HTTP verb → action mapping:
instrument({
// ...
actions: {
'PATCH': 'MODIFY', // instead of default UPDATE
'POST': 'SUBMIT', // instead of default CREATE
},
});Topic map
Override how message queue topics resolve (for future Kafka/AMQP patches):
instrument({
// ...
topics: {
'order.created': { resourceType: 'ORDER', action: 'NOTIFY' },
'order.*': { resourceType: 'ORDER' }, // wildcard — action defaults to PUBLISH/CONSUME
'audit.log': null, // null = don't track
},
});Outbound map
Override how outbound HTTP calls resolve by hostname:
instrument({
// ...
outbound: {
'payment-service': { resourceType: 'PAYMENT', action: 'PROCESS' },
'notification-svc': { resourceType: 'NOTIFICATION', action: 'SEND' },
},
});Without the outbound map, a POST to http://payment-service:4001/api/charges would resolve to resourceType: CHARGE, action: CREATE. The override gives you PAYMENT, PROCESS.
Resource ID extraction
Control how the resource ID is extracted for each resource type:
instrument({
// ...
resourceId: {
// Per resource type
'ORDER': (ctx) => ctx.params?.id || ctx.result?.orderId,
'CUSTOMER': (ctx) => ctx.user?.id,
'INVOICE': (ctx) => `inv-${ctx.params?.id}`, // derived ID
// Default (applies when no type-specific rule matches)
'*': (ctx) => ctx.params?.id || ctx.result?.id || ctx.body?.id,
},
});Context object
The ctx object passed to every resolver contains:
| Field | Type | Description |
|---|---|---|
method | string | HTTP method (GET, POST, etc.) |
path | string | Request path (/api/orders/order-501) |
route | string | Express/Fastify route pattern (/api/orders/:id) |
params | object | Route params ({ id: 'order-501' }) |
body | any | Parsed request body |
headers | object | Request headers |
user | any | req.user (if auth middleware ran) |
statusCode | number | Response status code |
result | any | Parsed response body |
parent | object | Parent resource from nested routes ({ resourceType, resourceId }) |
model | string | ORM model name (Prisma context only) |
operation | string | ORM operation (Prisma context only) |
topic | string | Message topic (queue context only) |
direction | string | publish or consume (queue context only) |
hostname | string | Target hostname (outbound context only) |
Full override
The resolve() function is the escape hatch — it runs first and can override everything or skip tracking entirely:
instrument({
// ...
resolve: (ctx) => {
// Skip internal endpoints
if (ctx.path === '/api/internal/health') return null;
// Custom logic for admin routes
if (ctx.path.startsWith('/api/admin')) {
return { resourceType: 'ADMIN_ACTION', action: 'EXECUTE' };
}
// Return undefined to fall through to route map / conventions
},
});- Return an object to override (
{ resourceType, action, resourceId }— all optional, missing fields filled by convention) - Return
nullto skip tracking for this request - Return
undefined(or nothing) to fall through to the next resolution step
Excluding paths
Skip paths that shouldn't be tracked:
instrument({
// ...
exclude: [
'/health',
'/metrics',
'/api-docs',
'GET /api/config', // specific method + path
],
});Exclusions match by prefix — /api-docs also excludes /api-docs/swagger.json.
Summary
The principle:
- 80% of routes work with zero config — conventions handle it
- 15% need a one-liner in the route/model map
- 5% need a resolver function for complex logic
nullin any map means "don't track this" — explicit opt-out
| Priority | Source | Example |
|---|---|---|
| 1 | resolve() function | Custom logic, skip tracking |
| 2 | routes / models / topics / outbound maps | Declarative overrides |
| 3 | Convention | Path segment → type, HTTP verb → action |
| 4 | Fallback | UNKNOWN type |