@sailfish-ai/sf-veritas
v0.3.1
Published
A versatile Edge Runtime-compatible package for JavaScript and TypeScript backend systems and scripts.
Maintainers
Readme
Sailfish's Backend Data Collector for JavaScript and TypeScript
Sailfish's veritas library captures all of the data to enable engineers to properly debug issues.
Supported Frameworks
As of September 2025, we support the most widely-used JS/TS backend frameworks and others (in both JavaScript and TypeScript):
- Most-widely Used
- Node.js
- Next.js
- Express
- Angular
- NestJS
- NuxtJS
- Others
- Apollo Server
- Mercurius
- MeteorJS
- GraphQL over HTTP (formerly Express GraphQL)
Installation
Install the Package
Install via your package manager; for reference, here is how you can install our package via npm and yarn:
npm install --save @sailfish-ai/sf-veritasyarn add @sailfish-ai/sf-veritasIntegration
Once you have installed the sf-veritas package, it's time to integrate it into your project.
Arguments/Parameters
Below are the configuration parameters for sf-veritas. These options are passed as an object to the setupInterceptors function.
apiKey:string- This can be seen and copied from your Settings Configuration page in the "Sailfish Configuration" section.
serviceIdentifier:string- This identifies the service that is running.
- The value is extremely important because Sailfish uses the
serviceIdentifierto link sevices to the location in code. Without this, Sailfish can have issues locating code and automatically fixing issues.- e.g.
gitOrg/gitRepo/pathToFile
- e.g.
serviceVersion:string- This identifies the version of your service - this is a human-readable version and is not required.
gitSha:string- This is the Git SHA of your releases - populating this via this argument or an environment variable helps your team tie issues to code changes quickly.
serviceAdditionalMetadata:Record<string, string | number | boolean | null>- Add extra metadata such as cluster information or environment details.
domainsToNotPropagateHeadersTo:string[]- Prevents adding tracing headers (
X-Sf3-Rid) to certain domains.- Supports wildcard characters, subdomains, and paths.
- Prevents adding tracing headers (
nodeModulesToCollectLocalVariablesOn:string[](coming soon)- Specify packages or modules for capturing local variable values during errors or exceptions.
Integration
To integrate and start, place the following into your application's entry point, as early as possible:
import { setupInterceptors } from "@sailfish-ai/sf-veritas";
setupInterceptors({
apiKey: "your-api-key",
serviceIdentifier: "<gitOrg>/<gitRepo>/<pathToFile>",
domainsToNotPropagateHeadersTo: ["example.com"],
});Runtime Hooks for Local Development
For local development and debugging without a build step, sf-veritas provides runtime hooks that automatically instrument your code on-the-fly.
⚠️ Important: Runtime hooks are intended for development only. For production, use the build plugins (webpack, vite, rollup, esbuild, or tsc) which provide better performance.
Node 20+ with ESM Loader (Recommended)
Use the --import flag with the loader registration:
# With Node.js directly
node --import @sailfish-ai/sf-veritas/runtime/register-loader app.js
# With tsx (TypeScript)
tsx --import @sailfish-ai/sf-veritas/runtime/register-loader app.ts
# In package.json scripts
{
"scripts": {
"dev": "node --import @sailfish-ai/sf-veritas/runtime/register-loader src/app.js"
}
}Configuration
Runtime hooks respect the same configuration as build plugins:
Environment Variables:
SF_FUNCSPAN_CONSOLE_OUTPUT=true # Output function spans to console (for development)
SF_FUNCSPAN_JSONL_FILE=true # Write function spans to funcspans.jsonl (default)
SF_FUNCSPAN_JSONL_FILE=/path/to/spans.jsonl # Write function spans to custom JSONL file
SF_FUNCSPAN_SKIP_BACKEND=true # Skip sending spans to backend (for local-only)
SF_FUNCSPAN_DEBUG=true # Enable debug logging
SF_FUNCSPAN_SAMPLE_RATE=1.0 # Override sampling rate (0.0-1.0)
SF_FUNCSPAN_INCLUDE_NODE_MODULES=express,axios # Instrument specific packagesConsole Output Example:
When SF_FUNCSPAN_CONSOLE_OUTPUT=true is set, you'll see clean output like:
📊 Function Span: {
function: 'add (src/utils.ts:10:0)',
duration: '0.05ms',
arguments: { a: 2, b: 3 },
result: 5,
spanId: 'a05a6dea-90ac-4c86-9d74-7815d8de87d2',
parentSpanId: 'none'
}JSONL File Output:
When SF_FUNCSPAN_JSONL_FILE=/path/to/file.jsonl is set, each function span is written as a JSON line:
{"timestamp":"2025-11-27T18:06:26.821Z","functionName":"add","filePath":"src/utils.ts","line":10,"column":0,"duration":0.05,"arguments":{"a":2,"b":3},"result":5,"spanId":"abc123","parentSpanId":"parent-id","async":false}This format is ideal for log processing tools and can be easily analyzed with jq:
# Pretty print a span
cat spans.jsonl | head -1 | jq .
# Find slow functions
cat spans.jsonl | jq 'select(.duration > 100)'
# Group by function name
cat spans.jsonl | jq -s 'group_by(.functionName) | map({function: .[0].functionName, count: length})'.sailfish Configuration Files:
Create a .sailfish file in your project directory:
{
"default": {
"sample_rate": 1.0,
"capture_arguments": true,
"capture_return_value": true
}
}Programmatic API
You can also register hooks programmatically:
import { registerFuncspanRuntime } from '@sailfish-ai/sf-veritas/runtime/register';
// Must be called before importing any modules you want to instrument
registerFuncspanRuntime({
debug: true,
includeNodeModules: ['express'],
sampleRate: 1.0
});
// Now import your application code
import './app';Requirements
- Node.js 16.12+ (stable in Node 18+, recommended: Node 20+)
- For ESM loader: Node 20.6+ for full support
Limitations
- Runtime transformation adds ~20-50ms per module on first load
- Not recommended for production use
- Cannot transform
eval()orvm.runInContext()code - May conflict with other custom loaders
Features
Identify Users
You can also associate data (logs, exceptions, and network requests) with specific users using the identify method. This is particularly helpful for user-centric debugging.
Identify Signature
Here's the argument signature:
type Identify = (
userId: string,
traits: Record<string, any>,
override: boolean,
) => void;Identify Example
In order to identify, you will need to call the identify method. You can also force an override of stored values by setting the override argument to true.
import { identify } from "@sailfish-ai/sf-veritas";
identify("user-123", { lastAction: "2024-11-18" });Add or Update Metadata
In order to add or update metadata associated with specific users and the recordings, you can use the addOrUpdateMetadata method. This will associate all information with the recording by calling this method on any Node.js backend.
userId:string- This associates the logs (and recording, if there is an associated recording) with a specific user.
traits:Record<string, any>- This is a custom dictionary of traits to values for a specific user or customer. Everything is searchable!
traitsJson:string- This is a custom JSON, just like
traits, but using the JSON format.
- This is a custom JSON, just like
override:boolean- This tells whether any of the information we pass along should be overridden or not.
AddOrUpdateMetadata Signature
type AddOrUpdateMetadataFunction = (
userId: string,
traits: Record<string, any>,
override: boolean,
) => void;Add/Update Metadata Example
import { addOrUpdateMetadata } from "@sailfish-ai/sf-veritas";
addOrUpdateMetadata("user-123", { birthday: "2000-01-01" }, true);Technical Details
- Non-Blocking Execution: All operations are executed in non-blocking, isolated threads, ensuring less than 20 µs of overhead.
- Contextual Awareness: Tracks context across threads, processes, and async tasks to associate logs with the correct sequence of events.
By following these steps, you can seamlessly integrate sf-veritas into your Node.js project and start capturing LEaPS (Logs, Exceptions, and Print Statements) effectively!
Release Notes
0.3.1
Critical fix — GraphQL error passthrough
Resolves a regression where the global Error override created a fresh intermediate prototype instead of aliasing the native Error.prototype directly. This caused instanceof Error to return false for Error subclasses declared before SDK initialization — including GraphQLError — which in turn caused graphql-js's locatedError to wrap every resolver error with "Unexpected error value: …" and strip path / locations. The prototype is now aliased directly (CustomError.prototype = OriginalError.prototype), preserving the native prototype chain for every existing Error subclass.
Timeline
- Dec 19, 2024 —
patchFetchlands with a silent-swallowcatch (error) {}block. Latent; dormant while telemetry paths were reliable. - Feb 20, 2025 — Global
Erroroverride ships with theObject.createprototype quirk.instanceof Errorsilently returnsfalseforGraphQLErrorfrom this point on. - Apr 16, 2026 —
0.3.0stable ships to npm. Both latent bugs widely exposed. - Apr 21, 2026 (morning) — Design-partner customer reports
"Unexpected error value"wrapping withpath/locationsstripped. - Apr 21, 2026 (afternoon) — Offline reproduction rig built side-by-side against
0.3.0(broken) and the local fix (fixed); all customer symptoms reproduced. - Apr 21, 2026 (evening) —
0.3.1ships. Primary fix + 5 adjacentfetch/httpbugs closed + 327-test regression suite locked in.
Adjacent fixes uncovered during the audit
patchFetchnow rethrows native errors (previously swallowed — causingfetch()to resolve toundefined, which manifested downstream as"Cannot return null for non-nullable field …").- 400/403 retry policy is now configurable via the new
retryOnClientErroroption. Default ('all') matches pre-0.3.1 behavior — on a 400/403 response carrying our tracing header, every method retries once with the header stripped, so customers whose backends reject unknown headers at the edge see no visible impact. Customers whose backends may commit side effects before returning 4xx (payment processors, booking systems, inventory APIs) should setretryOnClientError: 'idempotent'(only GET/HEAD/OPTIONS retry) or'none'(never retry). Also configurable via theSF_RETRY_ON_CLIENT_ERRORenvironment variable. Requestinputs preservemethod,body,credentials,mode,redirect,signal,referrer,integrity,cachethrough the instrumentation clone.- Response-body capture moved off the critical path. Server-Sent Events and chunked responses no longer block
fetch()resolution. - Non-text response bodies (images, video, octet-stream, protobuf, etc.) are no longer decoded as UTF-8 garbage; they produce a size-only placeholder.
- Relative or malformed URLs (
fetch("/api/foo")in isomorphic code) pass through to the native fetch instead of throwing inside the wrapper. - Request body capture handles
URLSearchParams,Blob,FormData,ReadableStream,ArrayBuffer, and typed arrays with meaningful placeholders instead of"{}".
Framework coverage — end-to-end integration tests
Every mainstream server-side GraphQL framework is exercised through real HTTP (or real Web Request/Response) with the SDK loaded, so any regression in the request/response pipeline surfaces immediately in CI.
| Framework | Coverage |
| --- | --- |
| Apollo Server v4 | ✓ |
| Hapi + custom route | ✓ |
| Express + hand-rolled handler | ✓ |
| Fastify + Mercurius | ✓ |
| graphql-yoga | ✓ |
| Koa + graphql-js | ✓ |
| NestJS + @nestjs/platform-express | ✓ |
| Next.js App Router (Node runtime) | ✓ |
| Next.js App Router (Edge runtime) | ✓ |
| Raw graphql-js / fetch-in-resolver | ✓ |
Hardening
- 327 behavioral tests across 35 files (from 74 prior to the regression audit).
tsc --noEmitclean. - Memory-leak verification — 10,000-iteration scenarios for patched
fetch,console.log, andErrorthrow+catch, with heap deltas measured after forced GC. Observed deltas: +1.74 MB / +0.07 MB / +8 bytes — well under the 50 MB budget and consistent with zero SDK-side retention. - AsyncLocalStorage context isolation — concurrent requests across sessions receive distinct trace IDs with no cross-tenant bleed.
- Process-handler preservation — user-registered
uncaughtException/unhandledRejection/SIGINThandlers still fire after SDK initialization; SDK does not add to either handler count. - Native error metadata preservation —
err.code,err.errno,err.syscallsurvive theErroroverride forfs,dns, and all other Node built-ins.
