Context Propagation
How UOW IDs, user IDs, and timestamps are propagated across services and async boundaries.
UOW propagation
The auto-instrumentation generates a Unit of Work ID at the entry point (inbound HTTP request) and propagates it through:
- Within the process —
AsyncLocalStoragecarries the UOW across all async operations in the same request - Across services —
x-uowidheader is injected on every outboundfetch()call and everyhttp.request/https.requestcall (which covers axios, got, node-fetch, and any library built on Node's http module) - From upstream — if the incoming request has an
x-uowidheader, it's reused instead of generating a new one
This means a single user request that hits 3 services and 2 databases all gets correlated under one UOW — automatically.
Header propagation
Two headers are read from incoming requests and propagated on all outbound HTTP calls (fetch, http.request, https.request):
| Header | Read from incoming | Injected on outbound | Fallback |
|---|---|---|---|
x-uowid | Yes — reuses upstream UOW | Yes | Auto-generated UUID |
x-userid | Yes — sets initial userId | Yes (if resolved) | userId() resolver → req.user.id |
This means a request that flows through multiple instrumented services preserves the original UOW ID and user identity across the entire chain.
Other HTTP clients
The SDK automatically injects UOW headers on both native fetch() and Node's http.request/https.request. Since axios, got, and node-fetch all use http.request under the hood, they get header injection automatically — no configuration needed.
If you need explicit control, you can also use getContext() to access the current UOW and inject headers manually:
Axios
Axios uses http.request internally, so headers are injected automatically. If you prefer explicit control, use a request interceptor:
const axios = require('axios');
const { getContext } = require('@stdiolabs/provenance-sdk/auto');
axios.interceptors.request.use((config) => {
const ctx = getContext();
if (ctx.uowId) {
config.headers['x-uowid'] = ctx.uowId;
}
if (ctx.userId) {
config.headers['x-userid'] = ctx.userId;
}
return config;
});This runs before every axios request — the downstream service receives the same UOW ID and continues the trace.
Got
Got uses http.request internally, so headers are injected automatically. If you prefer explicit control, use a beforeRequest hook:
const got = require('got');
const { getContext } = require('@stdiolabs/provenance-sdk/auto');
const client = got.extend({
hooks: {
beforeRequest: [
(options) => {
const ctx = getContext();
if (ctx.uowId) {
options.headers['x-uowid'] = ctx.uowId;
}
if (ctx.userId) {
options.headers['x-userid'] = ctx.userId;
}
},
],
},
});
// Use `client` instead of `got` for outbound calls
await client.post('http://payment-service/api/charges', { json: { orderId: '123' } });node-fetch
node-fetch uses http.request internally, so headers are injected automatically. If you prefer explicit control, wrap it:
const nodeFetch = require('node-fetch');
const { getContext } = require('@stdiolabs/provenance-sdk/auto');
function fetch(url, options = {}) {
const ctx = getContext();
const headers = { ...options.headers };
if (ctx.uowId) headers['x-uowid'] = ctx.uowId;
if (ctx.userId) headers['x-userid'] = ctx.userId;
return nodeFetch(url, { ...options, headers });
}
// Use this `fetch` instead of importing node-fetch directly
await fetch('http://payment-service/api/charges', {
method: 'POST',
body: JSON.stringify({ orderId: '123' }),
});Reusable helper
If you use multiple HTTP clients, extract the header logic into a shared helper:
// lib/provenance-headers.js
const { getContext } = require('@stdiolabs/provenance-sdk/auto');
function getProvenanceHeaders() {
const ctx = getContext();
const headers = {};
if (ctx.uowId) headers['x-uowid'] = ctx.uowId;
if (ctx.userId) headers['x-userid'] = ctx.userId;
return headers;
}
module.exports = { getProvenanceHeaders };Then use it anywhere:
const { getProvenanceHeaders } = require('./lib/provenance-headers');
// Axios
await axios.post(url, data, { headers: getProvenanceHeaders() });
// Got
await got.post(url, { headers: getProvenanceHeaders(), json: data });
// http.request
http.request(url, { headers: { ...getProvenanceHeaders(), 'content-type': 'application/json' } });Interaction timestamps
Each interaction is timestamped at the moment it's captured — not when the request started, and not inherited from upstream:
| Capture point | When timestamp is generated |
|---|---|
| Prisma | When the ORM operation completes |
| Outbound fetch | When the outbound response returns |
| Inbound HTTP | When the response is sent |
Manual track() | When you call track() (you can set interactionTimestamp yourself) |
This means a single request that does a Prisma create, an outbound fetch, and then responds will produce 3 interactions with 3 different timestamps — reflecting the actual time each operation happened.
The timestamp is an ISO 8601 string generated via new Date().toISOString() and passed to the Provenance API as interactionTimestamp.