Provenance
EcosystemAuto-Instrumentation

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 processAsyncLocalStorage carries the UOW across all async operations in the same request
  • Across servicesx-uowid header is injected on every outbound fetch() call and every http.request/https.request call (which covers axios, got, node-fetch, and any library built on Node's http module)
  • From upstream — if the incoming request has an x-uowid header, 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):

HeaderRead from incomingInjected on outboundFallback
x-uowidYes — reuses upstream UOWYesAuto-generated UUID
x-useridYes — sets initial userIdYes (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 pointWhen timestamp is generated
PrismaWhen the ORM operation completes
Outbound fetchWhen the outbound response returns
Inbound HTTPWhen 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.