Skip to content

Show opaque marker when deserializing custom classes and instances in o11y - #809

Merged
TooTallNate merged 8 commits into
mainfrom
01-19-show_opaque_marker_when_deserializing_custom_classes_and_instances_in_o11y
Jan 20, 2026
Merged

Show opaque marker when deserializing custom classes and instances in o11y#809
TooTallNate merged 8 commits into
mainfrom
01-19-show_opaque_marker_when_deserializing_custom_classes_and_instances_in_o11y

Conversation

@TooTallNate

@TooTallNateTooTallNate commented Jan 19, 2026

Copy link
Copy Markdown
Member

Web example:

dark mode
Screenshot 2026-01-20 at 01 38 04

light mode
Screenshot 2026-01-20 at 01 38 09

CLI example:

Screenshot 2026-01-20 at 01 44 53

Added support for displaying custom class instances in the observability UI by showing opaque markers when deserializing custom classes.

What changed?

  • Added a CLASS_INSTANCE_REF_TYPE marker and related interfaces to represent serialized class instances that cannot be fully deserialized
  • Implemented helper functions to extract class names from class IDs and convert serialized instances to reference objects
  • Enhanced the stream print revivers to handle custom class instances and class references
  • Updated the attribute panel to recognize and display class instance references with a __class__ marker alongside the serialized data

How to test?

  1. Create a workflow that uses custom classes
  2. Run the workflow and view it in the observability UI
  3. Verify that custom class instances are displayed with their class names and serialized data
  4. Check that class references are shown as <class:ClassName> in the UI

Why make this change?

Previously, custom class instances couldn't be properly displayed in the observability UI because they weren't registered for deserialization in the o11y context. This change improves the developer experience by providing meaningful representations of custom classes and instances, making it easier to debug and understand workflow execution without requiring class registration.

@changeset-bot

changeset-botBot commented Jan 19, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: c889dd1

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 13 packages
NameType
@workflow/web-sharedPatch
@workflow/corePatch
@workflow/buildersPatch
@workflow/cliPatch
@workflow/docs-typecheckPatch
@workflow/nextPatch
@workflow/nitroPatch
workflowPatch
@workflow/astroPatch
@workflow/sveltekitPatch
@workflow/world-testingPatch
@workflow/nuxtPatch
@workflow/aiPatch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions

github-actionsBot commented Jan 19, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production435038473
❌ 💻 Local Development396232430
✅ 📦 Local Production398032430
✅ 🐘 Local Postgres398032430
✅ 🪟 Windows430043
❌ 🌍 Community Worlds162220184
Total1832241341990

❌ Failed Tests

💻 Local Development (2 failed)

sveltekit-stable (2 failed):

  • error handling error propagation step errors basic step error preserves message and stack trace
  • error handling error propagation step errors cross-file step error preserves message and function names in stack
🌍 Community Worlds (22 failed)

mongodb (1 failed):

  • webhookWorkflow

redis (1 failed):

  • webhookWorkflow

starter (19 failed):

  • addTenWorkflow
  • addTenWorkflow
  • error handling error propagation workflow errors nested function calls preserve message and stack trace
  • error handling error propagation workflow errors cross-file imports preserve message and stack trace
  • error handling error propagation step errors basic step error preserves message and stack trace
  • error handling error propagation step errors cross-file step error preserves message and function names in stack
  • error handling retry behavior regular Error retries until success
  • error handling retry behavior FatalError fails immediately without retries
  • error handling catchability FatalError can be caught and detected with FatalError.is()
  • hookCleanupTestWorkflow - hook token reuse after workflow completion
  • stepFunctionPassingWorkflow - step function references can be passed as arguments (without closure vars)
  • stepFunctionWithClosureWorkflow - step function with closure variables passed as argument
  • spawnWorkflowFromStepWorkflow - spawning a child workflow using start() inside a step
  • pathsAliasWorkflow - TypeScript path aliases resolve correctly
  • Calculator.calculate - static workflow method using static step methods from another class
  • AllInOneService.processNumber - static workflow method using sibling static step methods
  • ChainableService.processWithThis - static step methods using this to reference the class
  • thisSerializationWorkflow - step function invoked with .call() and .apply()
  • customSerializationWorkflow - custom class serialization with WORKFLOW_SERIALIZE/WORKFLOW_DESERIALIZE

turso (1 failed):

  • webhookWorkflow

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro3904
✅ example3904
✅ express3904
✅ fastify3904
✅ hono3904
✅ nextjs-turbopack4201
✅ nextjs-webpack4201
✅ nitro3904
✅ nuxt3904
✅ sveltekit3904
✅ vite3904
❌ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable3904
✅ express-stable3904
✅ fastify-stable3904
✅ hono-stable3904
✅ nextjs-turbopack-stable4300
✅ nextjs-webpack-stable4300
✅ nitro-stable3904
✅ nuxt-stable3904
❌ sveltekit-stable3724
✅ vite-stable3904
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable3904
✅ express-stable3904
✅ fastify-stable3904
✅ hono-stable3904
✅ nextjs-turbopack-stable4300
✅ nextjs-webpack-stable4300
✅ nitro-stable3904
✅ nuxt-stable3904
✅ sveltekit-stable3904
✅ vite-stable3904
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable3904
✅ express-stable3904
✅ fastify-stable3904
✅ hono-stable3904
✅ nextjs-turbopack-stable4300
✅ nextjs-webpack-stable4300
✅ nitro-stable3904
✅ nuxt-stable3904
✅ sveltekit-stable3904
✅ vite-stable3904
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack4300
❌ 🌍 Community Worlds
AppPassedFailedSkipped
✅ mongodb-dev300
❌ mongodb4210
✅ redis-dev300
❌ redis4210
✅ starter-dev300
❌ starter24190
✅ turso-dev300
❌ turso4210

📋 View full workflow run


Some E2E test jobs failed:

  • Vercel Prod: success
  • Local Dev: failure
  • Local Prod: success
  • Local Postgres: success
  • Windows: success

Check the workflow run for details.

@TooTallNateGraphite App

TooTallNate commented Jan 19, 2026

Copy link
Copy Markdown
MemberAuthor

@TooTallNate
TooTallNate marked this pull request as ready for review January 19, 2026 22:17
CopilotAI review requested due to automatic review settings January 19, 2026 22:17
@vercel

vercelBot commented Jan 19, 2026

Copy link
Copy Markdown
Contributor

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR enhances the observability UI to display custom class instances that cannot be fully deserialized because their classes are not registered in the o11y context. The implementation adds opaque markers for both class instances and class references, making it easier to debug workflows that use custom classes.

Changes:

  • Added CLASS_INSTANCE_REF_TYPE marker, ClassInstanceRef interface, and type guards to represent unregistered class instances
  • Implemented helper functions (extractClassName, serializedInstanceToRef, serializedClassToString) to convert serialized class data to display-friendly formats
  • Extended streamPrintRevivers to handle Instance and Class types by converting them to opaque markers
  • Updated UI transformation logic to display class instances inline with a __class__ marker alongside their serialized data

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 8 comments.

FileDescription
packages/core/src/observability.tsAdds ClassInstanceRef types, helper functions for class name extraction, and stream print revivers for Instance and Class types
packages/web-shared/src/sidebar/attribute-panel.tsxDuplicates ClassInstanceRef types for client-side use and updates transformValueForDisplay to handle class instance refs inline with class marker
.changeset/forty-tables-lick.mdDocuments the patch-level changes for both packages

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

'__type' in value &&
value.__type === CLASS_INSTANCE_REF_TYPE &&
'className' in value &&
typeof value.className === 'string'

CopilotAIJan 19, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The type guard isClassInstanceRef validates className but not classId. For consistency with isStreamRef which validates all required fields, and to ensure type safety, the function should also validate that classId exists and is a string.

Suggested change
typeofvalue.className==='string'
typeofvalue.className==='string'&&
'classId'invalue&&
typeofvalue.classId==='string'

Copilot uses AI. Check for mistakes.
Comment on lines +124 to +128
const extractClassName = (classId: string): string => {
if (!classId) return 'Unknown';
const parts = classId.split('/');
return parts[parts.length - 1] || classId;
};

CopilotAIJan 19, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The extractClassName function returns 'Unknown' when classId is falsy, but an empty string is falsy in JavaScript. When classId is an empty string, the function would return 'Unknown' without trying to split it. However, if the split produces an empty array or the last part is an empty string, it would fall back to returning the original classId. Consider handling the empty string case explicitly before the split operation to ensure consistent behavior.

Copilot uses AI. Check for mistakes.
Comment on lines +244 to +249
return {
__class__: v.className,
...(typeof v.data === 'object' && v.data !== null
? v.data
: { value: v.data }),
};

CopilotAIJan 19, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The spread operator on line 247 could potentially overwrite the class property if v.data is an object that contains a class key. This would result in the class name information being lost. Consider using a different marker name that's less likely to collide with actual object properties, or ensure the class property cannot be overwritten by spreading v.data after it instead of before.

Suggested change
return{
__class__: v.className,
...(typeofv.data==='object'&&v.data!==null
? v.data
: {value: v.data}),
};
returntypeofv.data==='object'&&v.data!==null
? {
...(v.dataasRecord<string,unknown>),
__class__: v.className,
}
: {
value: v.data,
__class__: v.className,
};

Copilot uses AI. Check for mistakes.
Comment on lines +244 to +248
return {
__class__: v.className,
...(typeof v.data === 'object' && v.data !== null
? v.data
: { value: v.data }),

CopilotAIJan 19, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The v.data object is spread directly without being transformed recursively. If v.data contains nested StreamRef or ClassInstanceRef objects, they won't be properly handled. The nested data should be recursively transformed before spreading to ensure all special references are properly converted.

Suggested change
return{
__class__: v.className,
...(typeofv.data==='object'&&v.data!==null
? v.data
: {value: v.data}),
consttransformedData=
typeofv.data==='object'&&v.data!==null
? transform(v.data)
: transform(v.data);
return{
__class__: v.className,
...(typeofv.data==='object'&&v.data!==null
? (transformedDataasRecord<string,unknown>)
: {value: transformedData}),

Copilot uses AI. Check for mistakes.
Comment on lines +151 to 157
const serializedClassToString = (value: { classId: string }): string => {
const className = extractClassName(value.classId);
return `<class:${className}>`;
};

/**
* This is an extra reviver for devalue that takes any streams that would be converted,

CopilotAIJan 19, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The serializedClassToString function doesn't validate that the input value has the expected structure. If value.classId is undefined or not a string, extractClassName will handle it, but it would be clearer to add explicit validation to ensure the function is called with the expected shape of data.

Suggested change
constserializedClassToString=(value: {classId: string}): string=>{
constclassName=extractClassName(value.classId);
return`<class:${className}>`;
};
/**
*Thisisanextrareviverfordevaluethattakesanystreamsthatwouldbeconverted,
*Convertaserializedclassreferencetoa string representation.
*ThisisusedforClasstype(theconstructorreferenceitself,notaninstance).
*/
constserializedClassToString=(value: unknown): string=>{
if(
!value||
typeofvalue!=='object'||
!('classId'invalue)||
typeof(valueas{classId: unknown}).classId!=='string'
){
return'<class:Unknown>';
}
constclassId=(valueas{classId: string}).classId;
constclassName=extractClassName(classId);
return`<class:${className}>`;
};

Copilot uses AI. Check for mistakes.
Comment threadpackages/core/src/observability.ts Outdated
Comment on lines +139 to +143
return {
__type: CLASS_INSTANCE_REF_TYPE,
className: extractClassName(value.classId),
classId: value.classId,
data: value.data,

CopilotAIJan 19, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The serializedInstanceToRef function doesn't validate that the input value has the expected structure. While the type annotation suggests the expected shape, adding runtime validation would make the function more robust, especially since it's called from the streamPrintRevivers which receives values of type any.

Suggested change
return{
__type: CLASS_INSTANCE_REF_TYPE,
className: extractClassName(value.classId),
classId: value.classId,
data: value.data,
// Runtime validation because this function is ultimately called with values of type `any`.
constisObject=value!==null&&typeofvalue==='object';
constclassId=isObject&&typeof(valueasany).classId==='string'
? (valueasany).classId
: '';
constdata=isObject ? (valueasany).data : value;
return{
__type: CLASS_INSTANCE_REF_TYPE,
className: extractClassName(classId),
classId,
data,

Copilot uses AI. Check for mistakes.
Comment on lines +120 to +154
/**
* Extract the class name from a classId.
* The classId format is typically "path/to/file/ClassName" so we extract the last segment.
*/
const extractClassName = (classId: string): string => {
if (!classId) return 'Unknown';
const parts = classId.split('/');
return parts[parts.length - 1] || classId;
};

/**
* Convert a serialized class instance to a ClassInstanceRef for o11y display.
* This allows viewing custom class instances in the UI without needing
* the class to be registered for deserialization.
*/
const serializedInstanceToRef = (value: {
classId: string;
data: unknown;
}): ClassInstanceRef => {
return {
__type: CLASS_INSTANCE_REF_TYPE,
className: extractClassName(value.classId),
classId: value.classId,
data: value.data,
};
};

/**
* Convert a serialized class reference to a string representation.
* This is used for Class type (the constructor reference itself, not an instance).
*/
const serializedClassToString = (value: { classId: string }): string => {
const className = extractClassName(value.classId);
return `<class:${className}>`;
};

CopilotAIJan 19, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new helper functions extractClassName, serializedInstanceToRef, and serializedClassToString lack test coverage. Given that other utility functions in the codebase have comprehensive test files (e.g., serialization.test.ts, parse-name.test.ts), consider adding tests to verify the behavior of these functions, especially edge cases like empty classId, classIds with different path separators, and handling of various data types.

Copilot uses AI. Check for mistakes.
Comment threadpackages/core/src/observability.ts Outdated
'__type' in value &&
value.__type === CLASS_INSTANCE_REF_TYPE &&
'className' in value &&
typeof value.className === 'string'

CopilotAIJan 19, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The type guard isClassInstanceRef validates className but not classId. For consistency with isStreamRef which validates all required fields, and to ensure type safety, the function should also validate that classId exists and is a string. This would prevent runtime errors if a malformed ClassInstanceRef object is encountered.

Suggested change
typeofvalue.className==='string'
typeofvalue.className==='string'&&
'classId'invalue&&
typeofvalue.classId==='string'

Copilot uses AI. Check for mistakes.
@TooTallNate
TooTallNateforce-pushed the 01-19-show_opaque_marker_when_deserializing_custom_classes_and_instances_in_o11y branch from 127cfec to c46327fCompareJanuary 19, 2026 22:26
@TooTallNate
TooTallNateforce-pushed the 01-09-add_support_for_custom_class_instance_serialization branch from baea85a to 4216347CompareJanuary 19, 2026 22:26
value: unknown
): { json: string; streamRefs: Map<string, StreamRef> } => {
): {
json: string;

@vercelvercelBotJan 19, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ClassInstanceRef transformation allows properties in v.data to overwrite the class marker, and doesn't recursively transform nested ClassInstanceRef objects within v.data

Fix on Vercel

* Used in o11y when a custom class instance is encountered but the class is not
* registered for deserialization.
*/
interface ClassInstanceRef {

@vercelvercelBotJan 19, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Type guard isClassInstanceRef doesn't validate classId and data properties required by ClassInstanceRef interface, leading to type unsafety

Fix on Vercel

Comment threadpackages/core/src/observability.ts Outdated
'__type' in value &&
value.__type === CLASS_INSTANCE_REF_TYPE &&
'className' in value &&
typeof value.className === 'string'

@vercelvercelBotJan 19, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The isClassInstanceRef type guard doesn't validate classId and data properties required by the ClassInstanceRef interface

Fix on Vercel

@TooTallNate
TooTallNateforce-pushed the 01-19-show_opaque_marker_when_deserializing_custom_classes_and_instances_in_o11y branch from a14fe6d to 04b36f5CompareJanuary 19, 2026 23:14
@github-actions

github-actionsBot commented Jan 19, 2026

Copy link
Copy Markdown
Contributor

📊 Benchmark Results

📈 Comparing against baseline from main branch. Green 🟢 = faster, Red 🔺 = slower.

workflow with no steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Starter🥇 Next.js (Turbopack)0.040s (-1.2%)1.014s (~)0.974s101.00x
💻 LocalNext.js (Turbopack)0.042s (+2.7%)1.019s (~)0.977s101.06x
💻 LocalNitro0.043s (-3.3%)1.007s (~)0.964s101.09x
💻 LocalExpress0.044s (-1.1%)1.007s (~)0.963s101.11x
🌐 RedisNext.js (Turbopack)0.045s (+11.9% 🔺)1.018s (~)0.973s101.14x
🌐 TursoNext.js (Turbopack)0.106s (-3.1%)1.013s (~)0.908s102.66x
🌐 MongoDBNext.js (Turbopack)0.122s (+123.6% 🔺)1.018s (~)0.895s103.08x
🐘 PostgresNext.js (Turbopack)0.176s (-44.7% 🟢)1.022s (~)0.845s104.44x
🐘 PostgresNitro0.337s (+6.8% 🔺)1.012s (~)0.675s108.49x
🐘 PostgresExpress0.338s (+5.0% 🔺)1.013s (~)0.675s108.52x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro0.575s (-80.7% 🟢)1.580s (-58.0% 🟢)1.005s101.00x
▲ VercelExpress0.689s (-83.2% 🟢)1.686s (-66.5% 🟢)0.997s101.20x
▲ VercelNext.js (Turbopack)0.744s (-66.1% 🟢)1.568s (-45.5% 🟢)0.824s101.29x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Starter🥇 Next.js (Turbopack)1.090s (~)2.010s (~)0.920s101.00x
💻 LocalNext.js (Turbopack)1.098s (+0.6%)2.012s (~)0.914s101.01x
💻 LocalExpress1.112s (~)2.006s (~)0.895s101.02x
🌐 RedisNext.js (Turbopack)1.113s (+0.8%)2.013s (~)0.900s101.02x
💻 LocalNitro1.115s (~)2.007s (~)0.892s101.02x
🌐 MongoDBNext.js (Turbopack)1.261s (-4.0%)2.016s (~)0.755s101.16x
🌐 TursoNext.js (Turbopack)1.305s (~)2.013s (~)0.707s101.20x
🐘 PostgresExpress2.185s (+1.0%)3.015s (~)0.830s102.00x
🐘 PostgresNitro2.194s (+0.7%)3.015s (~)0.821s102.01x
🐘 PostgresNext.js (Turbopack)2.322s (+35.2% 🔺)2.818s (+33.2% 🔺)0.496s102.13x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)2.682s (-67.8% 🟢)3.592s (-59.7% 🟢)0.910s101.00x
▲ VercelExpress2.694s (-60.3% 🟢)3.718s (-51.6% 🟢)1.024s101.00x
▲ VercelNitro2.712s (-61.9% 🟢)3.698s (-53.0% 🟢)0.987s101.01x

🔍 Observability: Next.js (Turbopack) | Express | Nitro

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Starter🥇 Next.js (Turbopack)10.606s (~)11.012s (~)0.406s51.00x
💻 LocalNext.js (Turbopack)10.664s (~)11.014s (~)0.350s51.01x
🌐 RedisNext.js (Turbopack)10.708s (~)11.021s (~)0.314s51.01x
💻 LocalExpress10.787s (~)11.010s (~)0.223s51.02x
💻 LocalNitro10.789s (~)11.009s (~)0.220s51.02x
🌐 MongoDBNext.js (Turbopack)12.119s (-1.1%)12.829s (-1.5%)0.710s51.14x
🌐 TursoNext.js (Turbopack)12.175s (~)13.020s (~)0.845s51.15x
🐘 PostgresNext.js (Turbopack)15.410s (+2.7%)15.838s (+1.3%)0.428s51.45x
🐘 PostgresNitro20.351s (~)21.029s (~)0.678s51.92x
🐘 PostgresExpress20.416s (~)21.034s (~)0.618s51.92x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express21.836s (-1.2%)22.549s (-1.4%)0.713s51.00x
▲ VercelNext.js (Turbopack)22.144s (-0.7%)22.815s (~)0.671s51.01x
▲ VercelNitro22.426s (+2.0%)23.192s (+1.9%)0.767s51.03x

🔍 Observability: Express | Next.js (Turbopack) | Nitro

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Starter🥇 Next.js (Turbopack)1.355s (~)2.008s (~)0.653s151.00x
🌐 RedisNext.js (Turbopack)1.374s (+1.7%)2.011s (~)0.637s151.01x
💻 LocalNext.js (Turbopack)1.392s (~)2.013s (~)0.621s151.03x
💻 LocalNitro1.417s (+0.9%)2.005s (~)0.588s151.05x
💻 LocalExpress1.420s (+0.6%)2.007s (~)0.587s151.05x
🐘 PostgresNext.js (Turbopack)1.877s (+5.9% 🔺)2.014s (~)0.136s151.39x
🌐 MongoDBNext.js (Turbopack)2.144s (+1.0%)3.014s (~)0.870s101.58x
🌐 TursoNext.js (Turbopack)2.222s (+1.3%)3.012s (~)0.790s101.64x
🐘 PostgresExpress2.363s (~)3.012s (~)0.649s101.74x
🐘 PostgresNitro2.487s (+3.1%)3.012s (~)0.525s101.84x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.936s (-8.1% 🟢)3.789s (-5.6% 🟢)0.853s81.00x
▲ VercelExpress3.069s (+6.4% 🔺)3.925s (+7.8% 🔺)0.856s81.05x
▲ VercelNext.js (Turbopack)3.214s (+7.1% 🔺)3.982s (+6.0% 🔺)0.768s81.09x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)2.040s (-1.4%)2.896s (+3.9%)0.855s111.00x
💻 LocalExpress2.208s (~)3.181s (~)0.973s101.08x
💻 LocalNitro2.241s (+1.4%)3.168s (~)0.928s101.10x
🌐 StarterNext.js (Turbopack)2.483s (~)3.008s (~)0.525s101.22x
🌐 RedisNext.js (Turbopack)2.492s (+0.6%)3.012s (-0.7%)0.520s101.22x
🐘 PostgresNext.js (Turbopack)2.550s (-5.2% 🟢)3.020s (-0.8%)0.470s101.25x
🐘 PostgresExpress2.876s (-6.9% 🟢)3.232s (-17.3% 🟢)0.356s101.41x
🐘 PostgresNitro2.955s (~)3.114s (-7.0% 🟢)0.159s101.45x
🌐 MongoDBNext.js (Turbopack)4.650s (-0.6%)5.185s (~)0.535s62.28x
🌐 TursoNext.js (Turbopack)4.851s (+1.7%)5.180s (~)0.329s62.38x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.997s (+11.6% 🔺)4.820s (+16.3% 🔺)0.823s71.00x
▲ VercelExpress4.022s (+28.6% 🔺)4.751s (+23.7% 🔺)0.729s71.01x
▲ VercelNext.js (Turbopack)4.660s (+40.1% 🔺)5.415s (+38.1% 🔺)0.755s61.17x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Starter🥇 Next.js (Turbopack)1.359s (+0.5%)2.007s (~)0.648s151.00x
🌐 RedisNext.js (Turbopack)1.367s (~)2.011s (~)0.644s151.01x
💻 LocalExpress1.406s (-1.4%)2.006s (~)0.600s151.03x
💻 LocalNext.js (Turbopack)1.409s (+1.3%)2.015s (~)0.605s151.04x
💻 LocalNitro1.428s (~)2.005s (~)0.577s151.05x
🐘 PostgresNitro1.557s (-23.4% 🟢)2.010s (-16.1% 🟢)0.452s151.15x
🐘 PostgresNext.js (Turbopack)1.676s (-25.9% 🟢)2.013s (-25.2% 🟢)0.337s151.23x
🐘 PostgresExpress1.836s (+3.1%)2.010s (-3.3%)0.174s151.35x
🌐 MongoDBNext.js (Turbopack)2.116s (-0.9%)3.019s (~)0.903s101.56x
🌐 TursoNext.js (Turbopack)2.219s (-1.2%)3.013s (~)0.794s101.63x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)2.837s (+2.4%)3.571s (-2.7%)0.735s91.00x
▲ VercelExpress3.139s (+7.3% 🔺)4.119s (+10.8% 🔺)0.980s81.11x
▲ VercelNitro3.218s (-5.2% 🟢)4.497s (+2.9%)1.279s71.13x

🔍 Observability: Next.js (Turbopack) | Express | Nitro

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express2.235s (+0.7%)3.197s (+0.6%)0.963s101.00x
💻 LocalNext.js (Turbopack)2.307s (+8.1% 🔺)3.142s (+3.7%)0.835s101.03x
💻 LocalNitro2.378s (+3.5%)3.317s (+2.9%)0.939s101.06x
🌐 StarterNext.js (Turbopack)2.459s (~)3.008s (~)0.549s101.10x
🐘 PostgresNitro2.497s (-18.3% 🟢)3.017s (-15.7% 🟢)0.520s101.12x
🌐 RedisNext.js (Turbopack)2.517s (+1.3%)3.012s (~)0.495s101.13x
🐘 PostgresNext.js (Turbopack)2.583s (-5.0%)3.029s (-0.8%)0.447s101.16x
🐘 PostgresExpress2.642s (-8.9% 🟢)3.018s (-6.5% 🟢)0.377s101.18x
🌐 MongoDBNext.js (Turbopack)4.711s (~)5.181s (~)0.470s62.11x
🌐 TursoNext.js (Turbopack)4.778s (+1.6%)5.183s (-3.1%)0.405s62.14x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.209s (+2.5%)3.778s (+0.8%)0.569s81.00x
▲ VercelExpress3.254s (~)3.848s (+2.4%)0.594s81.01x
▲ VercelNext.js (Turbopack)3.454s (+0.9%)4.032s (~)0.578s81.08x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🌐 Starter🥇 Next.js (Turbopack)0.129s (~)1.005s (~)0.000s (NaN%)1.011s (~)0.882s101.00x
💻 LocalNext.js (Turbopack)0.141s (-3.4%)1.003s (~)0.017s (-0.6%)1.028s (~)0.887s101.09x
🌐 RedisNext.js (Turbopack)0.159s (+11.6% 🔺)1.005s (~)0.000s (-100.0% 🟢)1.015s (~)0.855s101.23x
💻 LocalExpress0.176s (-0.7%)0.992s (~)0.015s (-1.9%)1.022s (~)0.846s101.36x
💻 LocalNitro0.182s (+1.7%)0.992s (~)0.015s (-0.7%)1.021s (~)0.838s101.41x
🌐 TursoNext.js (Turbopack)0.488s (+6.3% 🔺)0.965s (-3.0%)0.000s (+Infinity% 🔺)1.013s (~)0.524s103.78x
🌐 MongoDBNext.js (Turbopack)0.506s (-3.3%)0.941s (+1.7%)0.000s (+Infinity% 🔺)1.015s (~)0.509s103.92x
🐘 PostgresNext.js (Turbopack)0.673s (-29.9% 🟢)1.052s (-1.5%)0.000s (-100.0% 🟢)1.118s (-15.1% 🟢)0.444s105.21x
🐘 PostgresExpress2.313s (+3.0%)2.728s (-2.7%)0.000s (-100.0% 🟢)3.014s (~)0.700s1017.90x
🐘 PostgresNitro2.402s (+3.2%)2.639s (-2.9%)0.000s (+Infinity% 🔺)3.014s (~)0.611s1018.59x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)2.826s (~)3.323s (+6.2% 🔺)0.378s (-49.5% 🟢)4.114s (-4.7%)1.288s101.00x
▲ VercelNitro2.891s (-0.7%)3.301s (+3.4%)0.454s (-23.0% 🟢)4.226s (-0.6%)1.335s101.02x
▲ VercelExpress2.914s (+1.5%)3.295s (+3.9%)0.396s (-58.0% 🟢)4.190s (-8.4% 🟢)1.276s101.03x

🔍 Observability: Next.js (Turbopack) | Nitro | Express

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalNext.js (Turbopack)6/8
🐘 PostgresNext.js (Turbopack)5/8
▲ VercelNitro4/8
Fastest World by Framework

Winner determined by most benchmark wins

Framework🥇 Fastest WorldWins
Express💻 Local8/8
Next.js (Turbopack)🌐 Starter6/8
Nitro💻 Local8/8
Column Definitions
  • Workflow Time: Runtime reported by workflow (completedAt - createdAt) - primary metric
  • TTFB: Time to First Byte - time from workflow start until first stream byte received (stream benchmarks only)
  • Slurp: Time from first byte to complete stream consumption (stream benchmarks only)
  • Wall Time: Total testbench time (trigger workflow + poll for result)
  • Overhead: Testbench overhead (Wall Time - Workflow Time)
  • Samples: Number of benchmark iterations run
  • vs Fastest: How much slower compared to the fastest configuration for this benchmark

Worlds:

  • 💻 Local: In-memory filesystem world (local development)
  • 🐘 Postgres: PostgreSQL database world (local development)
  • ▲ Vercel: Vercel production/preview deployment
  • 🌐 Starter: Community world (local development)
  • 🌐 Turso: Community world (local development)
  • 🌐 MongoDB: Community world (local development)
  • 🌐 Redis: Community world (local development)
  • 🌐 Jazz: Community world (local development)

📋 View full workflow run

@VaguelySeriousVaguelySerious left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM ✨

One styling change:

className="inline-flex flex-col rounded text-[11px] font-mono my-1"
style={{
backgroundColor: colors.body,
border: `1px solid ${colors.header}`,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This leaves some weird extra white pixel borders

Suggested change
border: `1px solid ${colors.header}`,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually I tested too early, this seems to already be resolved

@socket-security

socket-securityBot commented Jan 20, 2026

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

DiffPackageSupply Chain
Security
VulnerabilityQualityMaintenanceLicense
Addednpm/​@​types/​color-hash@​2.0.01001009080100
Addednpm/​color-hash@​2.0.210010010080100

View full report

Comment thread.changeset/forty-tables-lick.md Outdated
TooTallNateand others added 2 commits January 20, 2026 01:39
Co-authored-by: Peter Wielander <mittgfu@gmail.com>
Signed-off-by: Nathan Rajlich <n@n8.io>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@TooTallNate@VaguelySerious
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Show opaque marker when deserializing custom classes and instances in o11y by TooTallNate · Pull Request #809 · vercel/workflow · GitHub
Skip to content

Show opaque marker when deserializing custom classes and instances in o11y - #809

Merged
TooTallNate merged 8 commits into
mainfrom
01-19-show_opaque_marker_when_deserializing_custom_classes_and_instances_in_o11y
Jan 20, 2026
Merged

Show opaque marker when deserializing custom classes and instances in o11y#809
TooTallNate merged 8 commits into
mainfrom
01-19-show_opaque_marker_when_deserializing_custom_classes_and_instances_in_o11y

Conversation

@TooTallNate

@TooTallNateTooTallNate commented Jan 19, 2026

Copy link
Copy Markdown
Member

Web example:

dark mode
Screenshot 2026-01-20 at 01 38 04

light mode
Screenshot 2026-01-20 at 01 38 09

CLI example:

Screenshot 2026-01-20 at 01 44 53

Added support for displaying custom class instances in the observability UI by showing opaque markers when deserializing custom classes.

What changed?

  • Added a CLASS_INSTANCE_REF_TYPE marker and related interfaces to represent serialized class instances that cannot be fully deserialized
  • Implemented helper functions to extract class names from class IDs and convert serialized instances to reference objects
  • Enhanced the stream print revivers to handle custom class instances and class references
  • Updated the attribute panel to recognize and display class instance references with a __class__ marker alongside the serialized data

How to test?

  1. Create a workflow that uses custom classes
  2. Run the workflow and view it in the observability UI
  3. Verify that custom class instances are displayed with their class names and serialized data
  4. Check that class references are shown as <class:ClassName> in the UI

Why make this change?

Previously, custom class instances couldn't be properly displayed in the observability UI because they weren't registered for deserialization in the o11y context. This change improves the developer experience by providing meaningful representations of custom classes and instances, making it easier to debug and understand workflow execution without requiring class registration.

@changeset-bot

changeset-botBot commented Jan 19, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: c889dd1

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 13 packages
NameType
@workflow/web-sharedPatch
@workflow/corePatch
@workflow/buildersPatch
@workflow/cliPatch
@workflow/docs-typecheckPatch
@workflow/nextPatch
@workflow/nitroPatch
workflowPatch
@workflow/astroPatch
@workflow/sveltekitPatch
@workflow/world-testingPatch
@workflow/nuxtPatch
@workflow/aiPatch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions

github-actionsBot commented Jan 19, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production435038473
❌ 💻 Local Development396232430
✅ 📦 Local Production398032430
✅ 🐘 Local Postgres398032430
✅ 🪟 Windows430043
❌ 🌍 Community Worlds162220184
Total1832241341990

❌ Failed Tests

💻 Local Development (2 failed)

sveltekit-stable (2 failed):

  • error handling error propagation step errors basic step error preserves message and stack trace
  • error handling error propagation step errors cross-file step error preserves message and function names in stack
🌍 Community Worlds (22 failed)

mongodb (1 failed):

  • webhookWorkflow

redis (1 failed):

  • webhookWorkflow

starter (19 failed):

  • addTenWorkflow
  • addTenWorkflow
  • error handling error propagation workflow errors nested function calls preserve message and stack trace
  • error handling error propagation workflow errors cross-file imports preserve message and stack trace
  • error handling error propagation step errors basic step error preserves message and stack trace
  • error handling error propagation step errors cross-file step error preserves message and function names in stack
  • error handling retry behavior regular Error retries until success
  • error handling retry behavior FatalError fails immediately without retries
  • error handling catchability FatalError can be caught and detected with FatalError.is()
  • hookCleanupTestWorkflow - hook token reuse after workflow completion
  • stepFunctionPassingWorkflow - step function references can be passed as arguments (without closure vars)
  • stepFunctionWithClosureWorkflow - step function with closure variables passed as argument
  • spawnWorkflowFromStepWorkflow - spawning a child workflow using start() inside a step
  • pathsAliasWorkflow - TypeScript path aliases resolve correctly
  • Calculator.calculate - static workflow method using static step methods from another class
  • AllInOneService.processNumber - static workflow method using sibling static step methods
  • ChainableService.processWithThis - static step methods using this to reference the class
  • thisSerializationWorkflow - step function invoked with .call() and .apply()
  • customSerializationWorkflow - custom class serialization with WORKFLOW_SERIALIZE/WORKFLOW_DESERIALIZE

turso (1 failed):

  • webhookWorkflow

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro3904
✅ example3904
✅ express3904
✅ fastify3904
✅ hono3904
✅ nextjs-turbopack4201
✅ nextjs-webpack4201
✅ nitro3904
✅ nuxt3904
✅ sveltekit3904
✅ vite3904
❌ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable3904
✅ express-stable3904
✅ fastify-stable3904
✅ hono-stable3904
✅ nextjs-turbopack-stable4300
✅ nextjs-webpack-stable4300
✅ nitro-stable3904
✅ nuxt-stable3904
❌ sveltekit-stable3724
✅ vite-stable3904
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable3904
✅ express-stable3904
✅ fastify-stable3904
✅ hono-stable3904
✅ nextjs-turbopack-stable4300
✅ nextjs-webpack-stable4300
✅ nitro-stable3904
✅ nuxt-stable3904
✅ sveltekit-stable3904
✅ vite-stable3904
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable3904
✅ express-stable3904
✅ fastify-stable3904
✅ hono-stable3904
✅ nextjs-turbopack-stable4300
✅ nextjs-webpack-stable4300
✅ nitro-stable3904
✅ nuxt-stable3904
✅ sveltekit-stable3904
✅ vite-stable3904
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack4300
❌ 🌍 Community Worlds
AppPassedFailedSkipped
✅ mongodb-dev300
❌ mongodb4210
✅ redis-dev300
❌ redis4210
✅ starter-dev300
❌ starter24190
✅ turso-dev300
❌ turso4210

📋 View full workflow run


Some E2E test jobs failed:

  • Vercel Prod: success
  • Local Dev: failure
  • Local Prod: success
  • Local Postgres: success
  • Windows: success

Check the workflow run for details.

@TooTallNateGraphite App

TooTallNate commented Jan 19, 2026

Copy link
Copy Markdown
MemberAuthor

@TooTallNate
TooTallNate marked this pull request as ready for review January 19, 2026 22:17
CopilotAI review requested due to automatic review settings January 19, 2026 22:17
@vercel

vercelBot commented Jan 19, 2026

Copy link
Copy Markdown
Contributor

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR enhances the observability UI to display custom class instances that cannot be fully deserialized because their classes are not registered in the o11y context. The implementation adds opaque markers for both class instances and class references, making it easier to debug workflows that use custom classes.

Changes:

  • Added CLASS_INSTANCE_REF_TYPE marker, ClassInstanceRef interface, and type guards to represent unregistered class instances
  • Implemented helper functions (extractClassName, serializedInstanceToRef, serializedClassToString) to convert serialized class data to display-friendly formats
  • Extended streamPrintRevivers to handle Instance and Class types by converting them to opaque markers
  • Updated UI transformation logic to display class instances inline with a __class__ marker alongside their serialized data

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 8 comments.

FileDescription
packages/core/src/observability.tsAdds ClassInstanceRef types, helper functions for class name extraction, and stream print revivers for Instance and Class types
packages/web-shared/src/sidebar/attribute-panel.tsxDuplicates ClassInstanceRef types for client-side use and updates transformValueForDisplay to handle class instance refs inline with class marker
.changeset/forty-tables-lick.mdDocuments the patch-level changes for both packages

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

'__type' in value &&
value.__type === CLASS_INSTANCE_REF_TYPE &&
'className' in value &&
typeof value.className === 'string'

CopilotAIJan 19, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The type guard isClassInstanceRef validates className but not classId. For consistency with isStreamRef which validates all required fields, and to ensure type safety, the function should also validate that classId exists and is a string.

Suggested change
typeofvalue.className==='string'
typeofvalue.className==='string'&&
'classId'invalue&&
typeofvalue.classId==='string'

Copilot uses AI. Check for mistakes.
Comment on lines +124 to +128
const extractClassName = (classId: string): string => {
if (!classId) return 'Unknown';
const parts = classId.split('/');
return parts[parts.length - 1] || classId;
};

CopilotAIJan 19, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The extractClassName function returns 'Unknown' when classId is falsy, but an empty string is falsy in JavaScript. When classId is an empty string, the function would return 'Unknown' without trying to split it. However, if the split produces an empty array or the last part is an empty string, it would fall back to returning the original classId. Consider handling the empty string case explicitly before the split operation to ensure consistent behavior.

Copilot uses AI. Check for mistakes.
Comment on lines +244 to +249
return {
__class__: v.className,
...(typeof v.data === 'object' && v.data !== null
? v.data
: { value: v.data }),
};

CopilotAIJan 19, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The spread operator on line 247 could potentially overwrite the class property if v.data is an object that contains a class key. This would result in the class name information being lost. Consider using a different marker name that's less likely to collide with actual object properties, or ensure the class property cannot be overwritten by spreading v.data after it instead of before.

Suggested change
return{
__class__: v.className,
...(typeofv.data==='object'&&v.data!==null
? v.data
: {value: v.data}),
};
returntypeofv.data==='object'&&v.data!==null
? {
...(v.dataasRecord<string,unknown>),
__class__: v.className,
}
: {
value: v.data,
__class__: v.className,
};

Copilot uses AI. Check for mistakes.
Comment on lines +244 to +248
return {
__class__: v.className,
...(typeof v.data === 'object' && v.data !== null
? v.data
: { value: v.data }),

CopilotAIJan 19, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The v.data object is spread directly without being transformed recursively. If v.data contains nested StreamRef or ClassInstanceRef objects, they won't be properly handled. The nested data should be recursively transformed before spreading to ensure all special references are properly converted.

Suggested change
return{
__class__: v.className,
...(typeofv.data==='object'&&v.data!==null
? v.data
: {value: v.data}),
consttransformedData=
typeofv.data==='object'&&v.data!==null
? transform(v.data)
: transform(v.data);
return{
__class__: v.className,
...(typeofv.data==='object'&&v.data!==null
? (transformedDataasRecord<string,unknown>)
: {value: transformedData}),

Copilot uses AI. Check for mistakes.
Comment on lines +151 to 157
const serializedClassToString = (value: { classId: string }): string => {
const className = extractClassName(value.classId);
return `<class:${className}>`;
};

/**
* This is an extra reviver for devalue that takes any streams that would be converted,

CopilotAIJan 19, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The serializedClassToString function doesn't validate that the input value has the expected structure. If value.classId is undefined or not a string, extractClassName will handle it, but it would be clearer to add explicit validation to ensure the function is called with the expected shape of data.

Suggested change
constserializedClassToString=(value: {classId: string}): string=>{
constclassName=extractClassName(value.classId);
return`<class:${className}>`;
};
/**
*Thisisanextrareviverfordevaluethattakesanystreamsthatwouldbeconverted,
*Convertaserializedclassreferencetoa string representation.
*ThisisusedforClasstype(theconstructorreferenceitself,notaninstance).
*/
constserializedClassToString=(value: unknown): string=>{
if(
!value||
typeofvalue!=='object'||
!('classId'invalue)||
typeof(valueas{classId: unknown}).classId!=='string'
){
return'<class:Unknown>';
}
constclassId=(valueas{classId: string}).classId;
constclassName=extractClassName(classId);
return`<class:${className}>`;
};

Copilot uses AI. Check for mistakes.
Comment threadpackages/core/src/observability.ts Outdated
Comment on lines +139 to +143
return {
__type: CLASS_INSTANCE_REF_TYPE,
className: extractClassName(value.classId),
classId: value.classId,
data: value.data,

CopilotAIJan 19, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The serializedInstanceToRef function doesn't validate that the input value has the expected structure. While the type annotation suggests the expected shape, adding runtime validation would make the function more robust, especially since it's called from the streamPrintRevivers which receives values of type any.

Suggested change
return{
__type: CLASS_INSTANCE_REF_TYPE,
className: extractClassName(value.classId),
classId: value.classId,
data: value.data,
// Runtime validation because this function is ultimately called with values of type `any`.
constisObject=value!==null&&typeofvalue==='object';
constclassId=isObject&&typeof(valueasany).classId==='string'
? (valueasany).classId
: '';
constdata=isObject ? (valueasany).data : value;
return{
__type: CLASS_INSTANCE_REF_TYPE,
className: extractClassName(classId),
classId,
data,

Copilot uses AI. Check for mistakes.
Comment on lines +120 to +154
/**
* Extract the class name from a classId.
* The classId format is typically "path/to/file/ClassName" so we extract the last segment.
*/
const extractClassName = (classId: string): string => {
if (!classId) return 'Unknown';
const parts = classId.split('/');
return parts[parts.length - 1] || classId;
};

/**
* Convert a serialized class instance to a ClassInstanceRef for o11y display.
* This allows viewing custom class instances in the UI without needing
* the class to be registered for deserialization.
*/
const serializedInstanceToRef = (value: {
classId: string;
data: unknown;
}): ClassInstanceRef => {
return {
__type: CLASS_INSTANCE_REF_TYPE,
className: extractClassName(value.classId),
classId: value.classId,
data: value.data,
};
};

/**
* Convert a serialized class reference to a string representation.
* This is used for Class type (the constructor reference itself, not an instance).
*/
const serializedClassToString = (value: { classId: string }): string => {
const className = extractClassName(value.classId);
return `<class:${className}>`;
};

CopilotAIJan 19, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new helper functions extractClassName, serializedInstanceToRef, and serializedClassToString lack test coverage. Given that other utility functions in the codebase have comprehensive test files (e.g., serialization.test.ts, parse-name.test.ts), consider adding tests to verify the behavior of these functions, especially edge cases like empty classId, classIds with different path separators, and handling of various data types.

Copilot uses AI. Check for mistakes.
Comment threadpackages/core/src/observability.ts Outdated
'__type' in value &&
value.__type === CLASS_INSTANCE_REF_TYPE &&
'className' in value &&
typeof value.className === 'string'

CopilotAIJan 19, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The type guard isClassInstanceRef validates className but not classId. For consistency with isStreamRef which validates all required fields, and to ensure type safety, the function should also validate that classId exists and is a string. This would prevent runtime errors if a malformed ClassInstanceRef object is encountered.

Suggested change
typeofvalue.className==='string'
typeofvalue.className==='string'&&
'classId'invalue&&
typeofvalue.classId==='string'

Copilot uses AI. Check for mistakes.
@TooTallNate
TooTallNateforce-pushed the 01-19-show_opaque_marker_when_deserializing_custom_classes_and_instances_in_o11y branch from 127cfec to c46327fCompareJanuary 19, 2026 22:26
@TooTallNate
TooTallNateforce-pushed the 01-09-add_support_for_custom_class_instance_serialization branch from baea85a to 4216347CompareJanuary 19, 2026 22:26
value: unknown
): { json: string; streamRefs: Map<string, StreamRef> } => {
): {
json: string;

@vercelvercelBotJan 19, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ClassInstanceRef transformation allows properties in v.data to overwrite the class marker, and doesn't recursively transform nested ClassInstanceRef objects within v.data

Fix on Vercel

* Used in o11y when a custom class instance is encountered but the class is not
* registered for deserialization.
*/
interface ClassInstanceRef {

@vercelvercelBotJan 19, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Type guard isClassInstanceRef doesn't validate classId and data properties required by ClassInstanceRef interface, leading to type unsafety

Fix on Vercel

Comment threadpackages/core/src/observability.ts Outdated
'__type' in value &&
value.__type === CLASS_INSTANCE_REF_TYPE &&
'className' in value &&
typeof value.className === 'string'

@vercelvercelBotJan 19, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The isClassInstanceRef type guard doesn't validate classId and data properties required by the ClassInstanceRef interface

Fix on Vercel

@TooTallNate
TooTallNateforce-pushed the 01-19-show_opaque_marker_when_deserializing_custom_classes_and_instances_in_o11y branch from a14fe6d to 04b36f5CompareJanuary 19, 2026 23:14
@github-actions

github-actionsBot commented Jan 19, 2026

Copy link
Copy Markdown
Contributor

📊 Benchmark Results

📈 Comparing against baseline from main branch. Green 🟢 = faster, Red 🔺 = slower.

workflow with no steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Starter🥇 Next.js (Turbopack)0.040s (-1.2%)1.014s (~)0.974s101.00x
💻 LocalNext.js (Turbopack)0.042s (+2.7%)1.019s (~)0.977s101.06x
💻 LocalNitro0.043s (-3.3%)1.007s (~)0.964s101.09x
💻 LocalExpress0.044s (-1.1%)1.007s (~)0.963s101.11x
🌐 RedisNext.js (Turbopack)0.045s (+11.9% 🔺)1.018s (~)0.973s101.14x
🌐 TursoNext.js (Turbopack)0.106s (-3.1%)1.013s (~)0.908s102.66x
🌐 MongoDBNext.js (Turbopack)0.122s (+123.6% 🔺)1.018s (~)0.895s103.08x
🐘 PostgresNext.js (Turbopack)0.176s (-44.7% 🟢)1.022s (~)0.845s104.44x
🐘 PostgresNitro0.337s (+6.8% 🔺)1.012s (~)0.675s108.49x
🐘 PostgresExpress0.338s (+5.0% 🔺)1.013s (~)0.675s108.52x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro0.575s (-80.7% 🟢)1.580s (-58.0% 🟢)1.005s101.00x
▲ VercelExpress0.689s (-83.2% 🟢)1.686s (-66.5% 🟢)0.997s101.20x
▲ VercelNext.js (Turbopack)0.744s (-66.1% 🟢)1.568s (-45.5% 🟢)0.824s101.29x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Starter🥇 Next.js (Turbopack)1.090s (~)2.010s (~)0.920s101.00x
💻 LocalNext.js (Turbopack)1.098s (+0.6%)2.012s (~)0.914s101.01x
💻 LocalExpress1.112s (~)2.006s (~)0.895s101.02x
🌐 RedisNext.js (Turbopack)1.113s (+0.8%)2.013s (~)0.900s101.02x
💻 LocalNitro1.115s (~)2.007s (~)0.892s101.02x
🌐 MongoDBNext.js (Turbopack)1.261s (-4.0%)2.016s (~)0.755s101.16x
🌐 TursoNext.js (Turbopack)1.305s (~)2.013s (~)0.707s101.20x
🐘 PostgresExpress2.185s (+1.0%)3.015s (~)0.830s102.00x
🐘 PostgresNitro2.194s (+0.7%)3.015s (~)0.821s102.01x
🐘 PostgresNext.js (Turbopack)2.322s (+35.2% 🔺)2.818s (+33.2% 🔺)0.496s102.13x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)2.682s (-67.8% 🟢)3.592s (-59.7% 🟢)0.910s101.00x
▲ VercelExpress2.694s (-60.3% 🟢)3.718s (-51.6% 🟢)1.024s101.00x
▲ VercelNitro2.712s (-61.9% 🟢)3.698s (-53.0% 🟢)0.987s101.01x

🔍 Observability: Next.js (Turbopack) | Express | Nitro

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Starter🥇 Next.js (Turbopack)10.606s (~)11.012s (~)0.406s51.00x
💻 LocalNext.js (Turbopack)10.664s (~)11.014s (~)0.350s51.01x
🌐 RedisNext.js (Turbopack)10.708s (~)11.021s (~)0.314s51.01x
💻 LocalExpress10.787s (~)11.010s (~)0.223s51.02x
💻 LocalNitro10.789s (~)11.009s (~)0.220s51.02x
🌐 MongoDBNext.js (Turbopack)12.119s (-1.1%)12.829s (-1.5%)0.710s51.14x
🌐 TursoNext.js (Turbopack)12.175s (~)13.020s (~)0.845s51.15x
🐘 PostgresNext.js (Turbopack)15.410s (+2.7%)15.838s (+1.3%)0.428s51.45x
🐘 PostgresNitro20.351s (~)21.029s (~)0.678s51.92x
🐘 PostgresExpress20.416s (~)21.034s (~)0.618s51.92x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express21.836s (-1.2%)22.549s (-1.4%)0.713s51.00x
▲ VercelNext.js (Turbopack)22.144s (-0.7%)22.815s (~)0.671s51.01x
▲ VercelNitro22.426s (+2.0%)23.192s (+1.9%)0.767s51.03x

🔍 Observability: Express | Next.js (Turbopack) | Nitro

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Starter🥇 Next.js (Turbopack)1.355s (~)2.008s (~)0.653s151.00x
🌐 RedisNext.js (Turbopack)1.374s (+1.7%)2.011s (~)0.637s151.01x
💻 LocalNext.js (Turbopack)1.392s (~)2.013s (~)0.621s151.03x
💻 LocalNitro1.417s (+0.9%)2.005s (~)0.588s151.05x
💻 LocalExpress1.420s (+0.6%)2.007s (~)0.587s151.05x
🐘 PostgresNext.js (Turbopack)1.877s (+5.9% 🔺)2.014s (~)0.136s151.39x
🌐 MongoDBNext.js (Turbopack)2.144s (+1.0%)3.014s (~)0.870s101.58x
🌐 TursoNext.js (Turbopack)2.222s (+1.3%)3.012s (~)0.790s101.64x
🐘 PostgresExpress2.363s (~)3.012s (~)0.649s101.74x
🐘 PostgresNitro2.487s (+3.1%)3.012s (~)0.525s101.84x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.936s (-8.1% 🟢)3.789s (-5.6% 🟢)0.853s81.00x
▲ VercelExpress3.069s (+6.4% 🔺)3.925s (+7.8% 🔺)0.856s81.05x
▲ VercelNext.js (Turbopack)3.214s (+7.1% 🔺)3.982s (+6.0% 🔺)0.768s81.09x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)2.040s (-1.4%)2.896s (+3.9%)0.855s111.00x
💻 LocalExpress2.208s (~)3.181s (~)0.973s101.08x
💻 LocalNitro2.241s (+1.4%)3.168s (~)0.928s101.10x
🌐 StarterNext.js (Turbopack)2.483s (~)3.008s (~)0.525s101.22x
🌐 RedisNext.js (Turbopack)2.492s (+0.6%)3.012s (-0.7%)0.520s101.22x
🐘 PostgresNext.js (Turbopack)2.550s (-5.2% 🟢)3.020s (-0.8%)0.470s101.25x
🐘 PostgresExpress2.876s (-6.9% 🟢)3.232s (-17.3% 🟢)0.356s101.41x
🐘 PostgresNitro2.955s (~)3.114s (-7.0% 🟢)0.159s101.45x
🌐 MongoDBNext.js (Turbopack)4.650s (-0.6%)5.185s (~)0.535s62.28x
🌐 TursoNext.js (Turbopack)4.851s (+1.7%)5.180s (~)0.329s62.38x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.997s (+11.6% 🔺)4.820s (+16.3% 🔺)0.823s71.00x
▲ VercelExpress4.022s (+28.6% 🔺)4.751s (+23.7% 🔺)0.729s71.01x
▲ VercelNext.js (Turbopack)4.660s (+40.1% 🔺)5.415s (+38.1% 🔺)0.755s61.17x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Starter🥇 Next.js (Turbopack)1.359s (+0.5%)2.007s (~)0.648s151.00x
🌐 RedisNext.js (Turbopack)1.367s (~)2.011s (~)0.644s151.01x
💻 LocalExpress1.406s (-1.4%)2.006s (~)0.600s151.03x
💻 LocalNext.js (Turbopack)1.409s (+1.3%)2.015s (~)0.605s151.04x
💻 LocalNitro1.428s (~)2.005s (~)0.577s151.05x
🐘 PostgresNitro1.557s (-23.4% 🟢)2.010s (-16.1% 🟢)0.452s151.15x
🐘 PostgresNext.js (Turbopack)1.676s (-25.9% 🟢)2.013s (-25.2% 🟢)0.337s151.23x
🐘 PostgresExpress1.836s (+3.1%)2.010s (-3.3%)0.174s151.35x
🌐 MongoDBNext.js (Turbopack)2.116s (-0.9%)3.019s (~)0.903s101.56x
🌐 TursoNext.js (Turbopack)2.219s (-1.2%)3.013s (~)0.794s101.63x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)2.837s (+2.4%)3.571s (-2.7%)0.735s91.00x
▲ VercelExpress3.139s (+7.3% 🔺)4.119s (+10.8% 🔺)0.980s81.11x
▲ VercelNitro3.218s (-5.2% 🟢)4.497s (+2.9%)1.279s71.13x

🔍 Observability: Next.js (Turbopack) | Express | Nitro

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express2.235s (+0.7%)3.197s (+0.6%)0.963s101.00x
💻 LocalNext.js (Turbopack)2.307s (+8.1% 🔺)3.142s (+3.7%)0.835s101.03x
💻 LocalNitro2.378s (+3.5%)3.317s (+2.9%)0.939s101.06x
🌐 StarterNext.js (Turbopack)2.459s (~)3.008s (~)0.549s101.10x
🐘 PostgresNitro2.497s (-18.3% 🟢)3.017s (-15.7% 🟢)0.520s101.12x
🌐 RedisNext.js (Turbopack)2.517s (+1.3%)3.012s (~)0.495s101.13x
🐘 PostgresNext.js (Turbopack)2.583s (-5.0%)3.029s (-0.8%)0.447s101.16x
🐘 PostgresExpress2.642s (-8.9% 🟢)3.018s (-6.5% 🟢)0.377s101.18x
🌐 MongoDBNext.js (Turbopack)4.711s (~)5.181s (~)0.470s62.11x
🌐 TursoNext.js (Turbopack)4.778s (+1.6%)5.183s (-3.1%)0.405s62.14x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.209s (+2.5%)3.778s (+0.8%)0.569s81.00x
▲ VercelExpress3.254s (~)3.848s (+2.4%)0.594s81.01x
▲ VercelNext.js (Turbopack)3.454s (+0.9%)4.032s (~)0.578s81.08x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🌐 Starter🥇 Next.js (Turbopack)0.129s (~)1.005s (~)0.000s (NaN%)1.011s (~)0.882s101.00x
💻 LocalNext.js (Turbopack)0.141s (-3.4%)1.003s (~)0.017s (-0.6%)1.028s (~)0.887s101.09x
🌐 RedisNext.js (Turbopack)0.159s (+11.6% 🔺)1.005s (~)0.000s (-100.0% 🟢)1.015s (~)0.855s101.23x
💻 LocalExpress0.176s (-0.7%)0.992s (~)0.015s (-1.9%)1.022s (~)0.846s101.36x
💻 LocalNitro0.182s (+1.7%)0.992s (~)0.015s (-0.7%)1.021s (~)0.838s101.41x
🌐 TursoNext.js (Turbopack)0.488s (+6.3% 🔺)0.965s (-3.0%)0.000s (+Infinity% 🔺)1.013s (~)0.524s103.78x
🌐 MongoDBNext.js (Turbopack)0.506s (-3.3%)0.941s (+1.7%)0.000s (+Infinity% 🔺)1.015s (~)0.509s103.92x
🐘 PostgresNext.js (Turbopack)0.673s (-29.9% 🟢)1.052s (-1.5%)0.000s (-100.0% 🟢)1.118s (-15.1% 🟢)0.444s105.21x
🐘 PostgresExpress2.313s (+3.0%)2.728s (-2.7%)0.000s (-100.0% 🟢)3.014s (~)0.700s1017.90x
🐘 PostgresNitro2.402s (+3.2%)2.639s (-2.9%)0.000s (+Infinity% 🔺)3.014s (~)0.611s1018.59x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)2.826s (~)3.323s (+6.2% 🔺)0.378s (-49.5% 🟢)4.114s (-4.7%)1.288s101.00x
▲ VercelNitro2.891s (-0.7%)3.301s (+3.4%)0.454s (-23.0% 🟢)4.226s (-0.6%)1.335s101.02x
▲ VercelExpress2.914s (+1.5%)3.295s (+3.9%)0.396s (-58.0% 🟢)4.190s (-8.4% 🟢)1.276s101.03x

🔍 Observability: Next.js (Turbopack) | Nitro | Express

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalNext.js (Turbopack)6/8
🐘 PostgresNext.js (Turbopack)5/8
▲ VercelNitro4/8
Fastest World by Framework

Winner determined by most benchmark wins

Framework🥇 Fastest WorldWins
Express💻 Local8/8
Next.js (Turbopack)🌐 Starter6/8
Nitro💻 Local8/8
Column Definitions
  • Workflow Time: Runtime reported by workflow (completedAt - createdAt) - primary metric
  • TTFB: Time to First Byte - time from workflow start until first stream byte received (stream benchmarks only)
  • Slurp: Time from first byte to complete stream consumption (stream benchmarks only)
  • Wall Time: Total testbench time (trigger workflow + poll for result)
  • Overhead: Testbench overhead (Wall Time - Workflow Time)
  • Samples: Number of benchmark iterations run
  • vs Fastest: How much slower compared to the fastest configuration for this benchmark

Worlds:

  • 💻 Local: In-memory filesystem world (local development)
  • 🐘 Postgres: PostgreSQL database world (local development)
  • ▲ Vercel: Vercel production/preview deployment
  • 🌐 Starter: Community world (local development)
  • 🌐 Turso: Community world (local development)
  • 🌐 MongoDB: Community world (local development)
  • 🌐 Redis: Community world (local development)
  • 🌐 Jazz: Community world (local development)

📋 View full workflow run

@VaguelySeriousVaguelySerious left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM ✨

One styling change:

className="inline-flex flex-col rounded text-[11px] font-mono my-1"
style={{
backgroundColor: colors.body,
border: `1px solid ${colors.header}`,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This leaves some weird extra white pixel borders

Suggested change
border: `1px solid ${colors.header}`,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually I tested too early, this seems to already be resolved

@socket-security

socket-securityBot commented Jan 20, 2026

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

DiffPackageSupply Chain
Security
VulnerabilityQualityMaintenanceLicense
Addednpm/​@​types/​color-hash@​2.0.01001009080100
Addednpm/​color-hash@​2.0.210010010080100

View full report

Comment thread.changeset/forty-tables-lick.md Outdated
TooTallNateand others added 2 commits January 20, 2026 01:39
Co-authored-by: Peter Wielander <mittgfu@gmail.com>
Signed-off-by: Nathan Rajlich <n@n8.io>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@TooTallNate@VaguelySerious
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Show opaque marker when deserializing custom classes and instances in o11y by TooTallNate · Pull Request #809 · vercel/workflow · GitHub
Skip to content

Show opaque marker when deserializing custom classes and instances in o11y - #809

Merged
TooTallNate merged 8 commits into
mainfrom
01-19-show_opaque_marker_when_deserializing_custom_classes_and_instances_in_o11y
Jan 20, 2026
Merged

Show opaque marker when deserializing custom classes and instances in o11y#809
TooTallNate merged 8 commits into
mainfrom
01-19-show_opaque_marker_when_deserializing_custom_classes_and_instances_in_o11y

Conversation

@TooTallNate

@TooTallNateTooTallNate commented Jan 19, 2026

Copy link
Copy Markdown
Member

Web example:

dark mode
Screenshot 2026-01-20 at 01 38 04

light mode
Screenshot 2026-01-20 at 01 38 09

CLI example:

Screenshot 2026-01-20 at 01 44 53

Added support for displaying custom class instances in the observability UI by showing opaque markers when deserializing custom classes.

What changed?

  • Added a CLASS_INSTANCE_REF_TYPE marker and related interfaces to represent serialized class instances that cannot be fully deserialized
  • Implemented helper functions to extract class names from class IDs and convert serialized instances to reference objects
  • Enhanced the stream print revivers to handle custom class instances and class references
  • Updated the attribute panel to recognize and display class instance references with a __class__ marker alongside the serialized data

How to test?

  1. Create a workflow that uses custom classes
  2. Run the workflow and view it in the observability UI
  3. Verify that custom class instances are displayed with their class names and serialized data
  4. Check that class references are shown as <class:ClassName> in the UI

Why make this change?

Previously, custom class instances couldn't be properly displayed in the observability UI because they weren't registered for deserialization in the o11y context. This change improves the developer experience by providing meaningful representations of custom classes and instances, making it easier to debug and understand workflow execution without requiring class registration.

@changeset-bot

changeset-botBot commented Jan 19, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: c889dd1

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 13 packages
NameType
@workflow/web-sharedPatch
@workflow/corePatch
@workflow/buildersPatch
@workflow/cliPatch
@workflow/docs-typecheckPatch
@workflow/nextPatch
@workflow/nitroPatch
workflowPatch
@workflow/astroPatch
@workflow/sveltekitPatch
@workflow/world-testingPatch
@workflow/nuxtPatch
@workflow/aiPatch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions

github-actionsBot commented Jan 19, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production435038473
❌ 💻 Local Development396232430
✅ 📦 Local Production398032430
✅ 🐘 Local Postgres398032430
✅ 🪟 Windows430043
❌ 🌍 Community Worlds162220184
Total1832241341990

❌ Failed Tests

💻 Local Development (2 failed)

sveltekit-stable (2 failed):

  • error handling error propagation step errors basic step error preserves message and stack trace
  • error handling error propagation step errors cross-file step error preserves message and function names in stack
🌍 Community Worlds (22 failed)

mongodb (1 failed):

  • webhookWorkflow

redis (1 failed):

  • webhookWorkflow

starter (19 failed):

  • addTenWorkflow
  • addTenWorkflow
  • error handling error propagation workflow errors nested function calls preserve message and stack trace
  • error handling error propagation workflow errors cross-file imports preserve message and stack trace
  • error handling error propagation step errors basic step error preserves message and stack trace
  • error handling error propagation step errors cross-file step error preserves message and function names in stack
  • error handling retry behavior regular Error retries until success
  • error handling retry behavior FatalError fails immediately without retries
  • error handling catchability FatalError can be caught and detected with FatalError.is()
  • hookCleanupTestWorkflow - hook token reuse after workflow completion
  • stepFunctionPassingWorkflow - step function references can be passed as arguments (without closure vars)
  • stepFunctionWithClosureWorkflow - step function with closure variables passed as argument
  • spawnWorkflowFromStepWorkflow - spawning a child workflow using start() inside a step
  • pathsAliasWorkflow - TypeScript path aliases resolve correctly
  • Calculator.calculate - static workflow method using static step methods from another class
  • AllInOneService.processNumber - static workflow method using sibling static step methods
  • ChainableService.processWithThis - static step methods using this to reference the class
  • thisSerializationWorkflow - step function invoked with .call() and .apply()
  • customSerializationWorkflow - custom class serialization with WORKFLOW_SERIALIZE/WORKFLOW_DESERIALIZE

turso (1 failed):

  • webhookWorkflow

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro3904
✅ example3904
✅ express3904
✅ fastify3904
✅ hono3904
✅ nextjs-turbopack4201
✅ nextjs-webpack4201
✅ nitro3904
✅ nuxt3904
✅ sveltekit3904
✅ vite3904
❌ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable3904
✅ express-stable3904
✅ fastify-stable3904
✅ hono-stable3904
✅ nextjs-turbopack-stable4300
✅ nextjs-webpack-stable4300
✅ nitro-stable3904
✅ nuxt-stable3904
❌ sveltekit-stable3724
✅ vite-stable3904
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable3904
✅ express-stable3904
✅ fastify-stable3904
✅ hono-stable3904
✅ nextjs-turbopack-stable4300
✅ nextjs-webpack-stable4300
✅ nitro-stable3904
✅ nuxt-stable3904
✅ sveltekit-stable3904
✅ vite-stable3904
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable3904
✅ express-stable3904
✅ fastify-stable3904
✅ hono-stable3904
✅ nextjs-turbopack-stable4300
✅ nextjs-webpack-stable4300
✅ nitro-stable3904
✅ nuxt-stable3904
✅ sveltekit-stable3904
✅ vite-stable3904
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack4300
❌ 🌍 Community Worlds
AppPassedFailedSkipped
✅ mongodb-dev300
❌ mongodb4210
✅ redis-dev300
❌ redis4210
✅ starter-dev300
❌ starter24190
✅ turso-dev300
❌ turso4210

📋 View full workflow run


Some E2E test jobs failed:

  • Vercel Prod: success
  • Local Dev: failure
  • Local Prod: success
  • Local Postgres: success
  • Windows: success

Check the workflow run for details.

@TooTallNateGraphite App

TooTallNate commented Jan 19, 2026

Copy link
Copy Markdown
MemberAuthor

@TooTallNate
TooTallNate marked this pull request as ready for review January 19, 2026 22:17
CopilotAI review requested due to automatic review settings January 19, 2026 22:17
@vercel

vercelBot commented Jan 19, 2026

Copy link
Copy Markdown
Contributor

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR enhances the observability UI to display custom class instances that cannot be fully deserialized because their classes are not registered in the o11y context. The implementation adds opaque markers for both class instances and class references, making it easier to debug workflows that use custom classes.

Changes:

  • Added CLASS_INSTANCE_REF_TYPE marker, ClassInstanceRef interface, and type guards to represent unregistered class instances
  • Implemented helper functions (extractClassName, serializedInstanceToRef, serializedClassToString) to convert serialized class data to display-friendly formats
  • Extended streamPrintRevivers to handle Instance and Class types by converting them to opaque markers
  • Updated UI transformation logic to display class instances inline with a __class__ marker alongside their serialized data

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 8 comments.

FileDescription
packages/core/src/observability.tsAdds ClassInstanceRef types, helper functions for class name extraction, and stream print revivers for Instance and Class types
packages/web-shared/src/sidebar/attribute-panel.tsxDuplicates ClassInstanceRef types for client-side use and updates transformValueForDisplay to handle class instance refs inline with class marker
.changeset/forty-tables-lick.mdDocuments the patch-level changes for both packages

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

'__type' in value &&
value.__type === CLASS_INSTANCE_REF_TYPE &&
'className' in value &&
typeof value.className === 'string'

CopilotAIJan 19, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The type guard isClassInstanceRef validates className but not classId. For consistency with isStreamRef which validates all required fields, and to ensure type safety, the function should also validate that classId exists and is a string.

Suggested change
typeofvalue.className==='string'
typeofvalue.className==='string'&&
'classId'invalue&&
typeofvalue.classId==='string'

Copilot uses AI. Check for mistakes.
Comment on lines +124 to +128
const extractClassName = (classId: string): string => {
if (!classId) return 'Unknown';
const parts = classId.split('/');
return parts[parts.length - 1] || classId;
};

CopilotAIJan 19, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The extractClassName function returns 'Unknown' when classId is falsy, but an empty string is falsy in JavaScript. When classId is an empty string, the function would return 'Unknown' without trying to split it. However, if the split produces an empty array or the last part is an empty string, it would fall back to returning the original classId. Consider handling the empty string case explicitly before the split operation to ensure consistent behavior.

Copilot uses AI. Check for mistakes.
Comment on lines +244 to +249
return {
__class__: v.className,
...(typeof v.data === 'object' && v.data !== null
? v.data
: { value: v.data }),
};

CopilotAIJan 19, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The spread operator on line 247 could potentially overwrite the class property if v.data is an object that contains a class key. This would result in the class name information being lost. Consider using a different marker name that's less likely to collide with actual object properties, or ensure the class property cannot be overwritten by spreading v.data after it instead of before.

Suggested change
return{
__class__: v.className,
...(typeofv.data==='object'&&v.data!==null
? v.data
: {value: v.data}),
};
returntypeofv.data==='object'&&v.data!==null
? {
...(v.dataasRecord<string,unknown>),
__class__: v.className,
}
: {
value: v.data,
__class__: v.className,
};

Copilot uses AI. Check for mistakes.
Comment on lines +244 to +248
return {
__class__: v.className,
...(typeof v.data === 'object' && v.data !== null
? v.data
: { value: v.data }),

CopilotAIJan 19, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The v.data object is spread directly without being transformed recursively. If v.data contains nested StreamRef or ClassInstanceRef objects, they won't be properly handled. The nested data should be recursively transformed before spreading to ensure all special references are properly converted.

Suggested change
return{
__class__: v.className,
...(typeofv.data==='object'&&v.data!==null
? v.data
: {value: v.data}),
consttransformedData=
typeofv.data==='object'&&v.data!==null
? transform(v.data)
: transform(v.data);
return{
__class__: v.className,
...(typeofv.data==='object'&&v.data!==null
? (transformedDataasRecord<string,unknown>)
: {value: transformedData}),

Copilot uses AI. Check for mistakes.
Comment on lines +151 to 157
const serializedClassToString = (value: { classId: string }): string => {
const className = extractClassName(value.classId);
return `<class:${className}>`;
};

/**
* This is an extra reviver for devalue that takes any streams that would be converted,

CopilotAIJan 19, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The serializedClassToString function doesn't validate that the input value has the expected structure. If value.classId is undefined or not a string, extractClassName will handle it, but it would be clearer to add explicit validation to ensure the function is called with the expected shape of data.

Suggested change
constserializedClassToString=(value: {classId: string}): string=>{
constclassName=extractClassName(value.classId);
return`<class:${className}>`;
};
/**
*Thisisanextrareviverfordevaluethattakesanystreamsthatwouldbeconverted,
*Convertaserializedclassreferencetoa string representation.
*ThisisusedforClasstype(theconstructorreferenceitself,notaninstance).
*/
constserializedClassToString=(value: unknown): string=>{
if(
!value||
typeofvalue!=='object'||
!('classId'invalue)||
typeof(valueas{classId: unknown}).classId!=='string'
){
return'<class:Unknown>';
}
constclassId=(valueas{classId: string}).classId;
constclassName=extractClassName(classId);
return`<class:${className}>`;
};

Copilot uses AI. Check for mistakes.
Comment threadpackages/core/src/observability.ts Outdated
Comment on lines +139 to +143
return {
__type: CLASS_INSTANCE_REF_TYPE,
className: extractClassName(value.classId),
classId: value.classId,
data: value.data,

CopilotAIJan 19, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The serializedInstanceToRef function doesn't validate that the input value has the expected structure. While the type annotation suggests the expected shape, adding runtime validation would make the function more robust, especially since it's called from the streamPrintRevivers which receives values of type any.

Suggested change
return{
__type: CLASS_INSTANCE_REF_TYPE,
className: extractClassName(value.classId),
classId: value.classId,
data: value.data,
// Runtime validation because this function is ultimately called with values of type `any`.
constisObject=value!==null&&typeofvalue==='object';
constclassId=isObject&&typeof(valueasany).classId==='string'
? (valueasany).classId
: '';
constdata=isObject ? (valueasany).data : value;
return{
__type: CLASS_INSTANCE_REF_TYPE,
className: extractClassName(classId),
classId,
data,

Copilot uses AI. Check for mistakes.
Comment on lines +120 to +154
/**
* Extract the class name from a classId.
* The classId format is typically "path/to/file/ClassName" so we extract the last segment.
*/
const extractClassName = (classId: string): string => {
if (!classId) return 'Unknown';
const parts = classId.split('/');
return parts[parts.length - 1] || classId;
};

/**
* Convert a serialized class instance to a ClassInstanceRef for o11y display.
* This allows viewing custom class instances in the UI without needing
* the class to be registered for deserialization.
*/
const serializedInstanceToRef = (value: {
classId: string;
data: unknown;
}): ClassInstanceRef => {
return {
__type: CLASS_INSTANCE_REF_TYPE,
className: extractClassName(value.classId),
classId: value.classId,
data: value.data,
};
};

/**
* Convert a serialized class reference to a string representation.
* This is used for Class type (the constructor reference itself, not an instance).
*/
const serializedClassToString = (value: { classId: string }): string => {
const className = extractClassName(value.classId);
return `<class:${className}>`;
};

CopilotAIJan 19, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new helper functions extractClassName, serializedInstanceToRef, and serializedClassToString lack test coverage. Given that other utility functions in the codebase have comprehensive test files (e.g., serialization.test.ts, parse-name.test.ts), consider adding tests to verify the behavior of these functions, especially edge cases like empty classId, classIds with different path separators, and handling of various data types.

Copilot uses AI. Check for mistakes.
Comment threadpackages/core/src/observability.ts Outdated
'__type' in value &&
value.__type === CLASS_INSTANCE_REF_TYPE &&
'className' in value &&
typeof value.className === 'string'

CopilotAIJan 19, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The type guard isClassInstanceRef validates className but not classId. For consistency with isStreamRef which validates all required fields, and to ensure type safety, the function should also validate that classId exists and is a string. This would prevent runtime errors if a malformed ClassInstanceRef object is encountered.

Suggested change
typeofvalue.className==='string'
typeofvalue.className==='string'&&
'classId'invalue&&
typeofvalue.classId==='string'

Copilot uses AI. Check for mistakes.
@TooTallNate
TooTallNateforce-pushed the 01-19-show_opaque_marker_when_deserializing_custom_classes_and_instances_in_o11y branch from 127cfec to c46327fCompareJanuary 19, 2026 22:26
@TooTallNate
TooTallNateforce-pushed the 01-09-add_support_for_custom_class_instance_serialization branch from baea85a to 4216347CompareJanuary 19, 2026 22:26
value: unknown
): { json: string; streamRefs: Map<string, StreamRef> } => {
): {
json: string;

@vercelvercelBotJan 19, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ClassInstanceRef transformation allows properties in v.data to overwrite the class marker, and doesn't recursively transform nested ClassInstanceRef objects within v.data

Fix on Vercel

* Used in o11y when a custom class instance is encountered but the class is not
* registered for deserialization.
*/
interface ClassInstanceRef {

@vercelvercelBotJan 19, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Type guard isClassInstanceRef doesn't validate classId and data properties required by ClassInstanceRef interface, leading to type unsafety

Fix on Vercel

Comment threadpackages/core/src/observability.ts Outdated
'__type' in value &&
value.__type === CLASS_INSTANCE_REF_TYPE &&
'className' in value &&
typeof value.className === 'string'

@vercelvercelBotJan 19, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The isClassInstanceRef type guard doesn't validate classId and data properties required by the ClassInstanceRef interface

Fix on Vercel

@TooTallNate
TooTallNateforce-pushed the 01-19-show_opaque_marker_when_deserializing_custom_classes_and_instances_in_o11y branch from a14fe6d to 04b36f5CompareJanuary 19, 2026 23:14
@github-actions

github-actionsBot commented Jan 19, 2026

Copy link
Copy Markdown
Contributor

📊 Benchmark Results

📈 Comparing against baseline from main branch. Green 🟢 = faster, Red 🔺 = slower.

workflow with no steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Starter🥇 Next.js (Turbopack)0.040s (-1.2%)1.014s (~)0.974s101.00x
💻 LocalNext.js (Turbopack)0.042s (+2.7%)1.019s (~)0.977s101.06x
💻 LocalNitro0.043s (-3.3%)1.007s (~)0.964s101.09x
💻 LocalExpress0.044s (-1.1%)1.007s (~)0.963s101.11x
🌐 RedisNext.js (Turbopack)0.045s (+11.9% 🔺)1.018s (~)0.973s101.14x
🌐 TursoNext.js (Turbopack)0.106s (-3.1%)1.013s (~)0.908s102.66x
🌐 MongoDBNext.js (Turbopack)0.122s (+123.6% 🔺)1.018s (~)0.895s103.08x
🐘 PostgresNext.js (Turbopack)0.176s (-44.7% 🟢)1.022s (~)0.845s104.44x
🐘 PostgresNitro0.337s (+6.8% 🔺)1.012s (~)0.675s108.49x
🐘 PostgresExpress0.338s (+5.0% 🔺)1.013s (~)0.675s108.52x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro0.575s (-80.7% 🟢)1.580s (-58.0% 🟢)1.005s101.00x
▲ VercelExpress0.689s (-83.2% 🟢)1.686s (-66.5% 🟢)0.997s101.20x
▲ VercelNext.js (Turbopack)0.744s (-66.1% 🟢)1.568s (-45.5% 🟢)0.824s101.29x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Starter🥇 Next.js (Turbopack)1.090s (~)2.010s (~)0.920s101.00x
💻 LocalNext.js (Turbopack)1.098s (+0.6%)2.012s (~)0.914s101.01x
💻 LocalExpress1.112s (~)2.006s (~)0.895s101.02x
🌐 RedisNext.js (Turbopack)1.113s (+0.8%)2.013s (~)0.900s101.02x
💻 LocalNitro1.115s (~)2.007s (~)0.892s101.02x
🌐 MongoDBNext.js (Turbopack)1.261s (-4.0%)2.016s (~)0.755s101.16x
🌐 TursoNext.js (Turbopack)1.305s (~)2.013s (~)0.707s101.20x
🐘 PostgresExpress2.185s (+1.0%)3.015s (~)0.830s102.00x
🐘 PostgresNitro2.194s (+0.7%)3.015s (~)0.821s102.01x
🐘 PostgresNext.js (Turbopack)2.322s (+35.2% 🔺)2.818s (+33.2% 🔺)0.496s102.13x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)2.682s (-67.8% 🟢)3.592s (-59.7% 🟢)0.910s101.00x
▲ VercelExpress2.694s (-60.3% 🟢)3.718s (-51.6% 🟢)1.024s101.00x
▲ VercelNitro2.712s (-61.9% 🟢)3.698s (-53.0% 🟢)0.987s101.01x

🔍 Observability: Next.js (Turbopack) | Express | Nitro

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Starter🥇 Next.js (Turbopack)10.606s (~)11.012s (~)0.406s51.00x
💻 LocalNext.js (Turbopack)10.664s (~)11.014s (~)0.350s51.01x
🌐 RedisNext.js (Turbopack)10.708s (~)11.021s (~)0.314s51.01x
💻 LocalExpress10.787s (~)11.010s (~)0.223s51.02x
💻 LocalNitro10.789s (~)11.009s (~)0.220s51.02x
🌐 MongoDBNext.js (Turbopack)12.119s (-1.1%)12.829s (-1.5%)0.710s51.14x
🌐 TursoNext.js (Turbopack)12.175s (~)13.020s (~)0.845s51.15x
🐘 PostgresNext.js (Turbopack)15.410s (+2.7%)15.838s (+1.3%)0.428s51.45x
🐘 PostgresNitro20.351s (~)21.029s (~)0.678s51.92x
🐘 PostgresExpress20.416s (~)21.034s (~)0.618s51.92x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express21.836s (-1.2%)22.549s (-1.4%)0.713s51.00x
▲ VercelNext.js (Turbopack)22.144s (-0.7%)22.815s (~)0.671s51.01x
▲ VercelNitro22.426s (+2.0%)23.192s (+1.9%)0.767s51.03x

🔍 Observability: Express | Next.js (Turbopack) | Nitro

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Starter🥇 Next.js (Turbopack)1.355s (~)2.008s (~)0.653s151.00x
🌐 RedisNext.js (Turbopack)1.374s (+1.7%)2.011s (~)0.637s151.01x
💻 LocalNext.js (Turbopack)1.392s (~)2.013s (~)0.621s151.03x
💻 LocalNitro1.417s (+0.9%)2.005s (~)0.588s151.05x
💻 LocalExpress1.420s (+0.6%)2.007s (~)0.587s151.05x
🐘 PostgresNext.js (Turbopack)1.877s (+5.9% 🔺)2.014s (~)0.136s151.39x
🌐 MongoDBNext.js (Turbopack)2.144s (+1.0%)3.014s (~)0.870s101.58x
🌐 TursoNext.js (Turbopack)2.222s (+1.3%)3.012s (~)0.790s101.64x
🐘 PostgresExpress2.363s (~)3.012s (~)0.649s101.74x
🐘 PostgresNitro2.487s (+3.1%)3.012s (~)0.525s101.84x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.936s (-8.1% 🟢)3.789s (-5.6% 🟢)0.853s81.00x
▲ VercelExpress3.069s (+6.4% 🔺)3.925s (+7.8% 🔺)0.856s81.05x
▲ VercelNext.js (Turbopack)3.214s (+7.1% 🔺)3.982s (+6.0% 🔺)0.768s81.09x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)2.040s (-1.4%)2.896s (+3.9%)0.855s111.00x
💻 LocalExpress2.208s (~)3.181s (~)0.973s101.08x
💻 LocalNitro2.241s (+1.4%)3.168s (~)0.928s101.10x
🌐 StarterNext.js (Turbopack)2.483s (~)3.008s (~)0.525s101.22x
🌐 RedisNext.js (Turbopack)2.492s (+0.6%)3.012s (-0.7%)0.520s101.22x
🐘 PostgresNext.js (Turbopack)2.550s (-5.2% 🟢)3.020s (-0.8%)0.470s101.25x
🐘 PostgresExpress2.876s (-6.9% 🟢)3.232s (-17.3% 🟢)0.356s101.41x
🐘 PostgresNitro2.955s (~)3.114s (-7.0% 🟢)0.159s101.45x
🌐 MongoDBNext.js (Turbopack)4.650s (-0.6%)5.185s (~)0.535s62.28x
🌐 TursoNext.js (Turbopack)4.851s (+1.7%)5.180s (~)0.329s62.38x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.997s (+11.6% 🔺)4.820s (+16.3% 🔺)0.823s71.00x
▲ VercelExpress4.022s (+28.6% 🔺)4.751s (+23.7% 🔺)0.729s71.01x
▲ VercelNext.js (Turbopack)4.660s (+40.1% 🔺)5.415s (+38.1% 🔺)0.755s61.17x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Starter🥇 Next.js (Turbopack)1.359s (+0.5%)2.007s (~)0.648s151.00x
🌐 RedisNext.js (Turbopack)1.367s (~)2.011s (~)0.644s151.01x
💻 LocalExpress1.406s (-1.4%)2.006s (~)0.600s151.03x
💻 LocalNext.js (Turbopack)1.409s (+1.3%)2.015s (~)0.605s151.04x
💻 LocalNitro1.428s (~)2.005s (~)0.577s151.05x
🐘 PostgresNitro1.557s (-23.4% 🟢)2.010s (-16.1% 🟢)0.452s151.15x
🐘 PostgresNext.js (Turbopack)1.676s (-25.9% 🟢)2.013s (-25.2% 🟢)0.337s151.23x
🐘 PostgresExpress1.836s (+3.1%)2.010s (-3.3%)0.174s151.35x
🌐 MongoDBNext.js (Turbopack)2.116s (-0.9%)3.019s (~)0.903s101.56x
🌐 TursoNext.js (Turbopack)2.219s (-1.2%)3.013s (~)0.794s101.63x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)2.837s (+2.4%)3.571s (-2.7%)0.735s91.00x
▲ VercelExpress3.139s (+7.3% 🔺)4.119s (+10.8% 🔺)0.980s81.11x
▲ VercelNitro3.218s (-5.2% 🟢)4.497s (+2.9%)1.279s71.13x

🔍 Observability: Next.js (Turbopack) | Express | Nitro

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express2.235s (+0.7%)3.197s (+0.6%)0.963s101.00x
💻 LocalNext.js (Turbopack)2.307s (+8.1% 🔺)3.142s (+3.7%)0.835s101.03x
💻 LocalNitro2.378s (+3.5%)3.317s (+2.9%)0.939s101.06x
🌐 StarterNext.js (Turbopack)2.459s (~)3.008s (~)0.549s101.10x
🐘 PostgresNitro2.497s (-18.3% 🟢)3.017s (-15.7% 🟢)0.520s101.12x
🌐 RedisNext.js (Turbopack)2.517s (+1.3%)3.012s (~)0.495s101.13x
🐘 PostgresNext.js (Turbopack)2.583s (-5.0%)3.029s (-0.8%)0.447s101.16x
🐘 PostgresExpress2.642s (-8.9% 🟢)3.018s (-6.5% 🟢)0.377s101.18x
🌐 MongoDBNext.js (Turbopack)4.711s (~)5.181s (~)0.470s62.11x
🌐 TursoNext.js (Turbopack)4.778s (+1.6%)5.183s (-3.1%)0.405s62.14x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.209s (+2.5%)3.778s (+0.8%)0.569s81.00x
▲ VercelExpress3.254s (~)3.848s (+2.4%)0.594s81.01x
▲ VercelNext.js (Turbopack)3.454s (+0.9%)4.032s (~)0.578s81.08x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🌐 Starter🥇 Next.js (Turbopack)0.129s (~)1.005s (~)0.000s (NaN%)1.011s (~)0.882s101.00x
💻 LocalNext.js (Turbopack)0.141s (-3.4%)1.003s (~)0.017s (-0.6%)1.028s (~)0.887s101.09x
🌐 RedisNext.js (Turbopack)0.159s (+11.6% 🔺)1.005s (~)0.000s (-100.0% 🟢)1.015s (~)0.855s101.23x
💻 LocalExpress0.176s (-0.7%)0.992s (~)0.015s (-1.9%)1.022s (~)0.846s101.36x
💻 LocalNitro0.182s (+1.7%)0.992s (~)0.015s (-0.7%)1.021s (~)0.838s101.41x
🌐 TursoNext.js (Turbopack)0.488s (+6.3% 🔺)0.965s (-3.0%)0.000s (+Infinity% 🔺)1.013s (~)0.524s103.78x
🌐 MongoDBNext.js (Turbopack)0.506s (-3.3%)0.941s (+1.7%)0.000s (+Infinity% 🔺)1.015s (~)0.509s103.92x
🐘 PostgresNext.js (Turbopack)0.673s (-29.9% 🟢)1.052s (-1.5%)0.000s (-100.0% 🟢)1.118s (-15.1% 🟢)0.444s105.21x
🐘 PostgresExpress2.313s (+3.0%)2.728s (-2.7%)0.000s (-100.0% 🟢)3.014s (~)0.700s1017.90x
🐘 PostgresNitro2.402s (+3.2%)2.639s (-2.9%)0.000s (+Infinity% 🔺)3.014s (~)0.611s1018.59x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)2.826s (~)3.323s (+6.2% 🔺)0.378s (-49.5% 🟢)4.114s (-4.7%)1.288s101.00x
▲ VercelNitro2.891s (-0.7%)3.301s (+3.4%)0.454s (-23.0% 🟢)4.226s (-0.6%)1.335s101.02x
▲ VercelExpress2.914s (+1.5%)3.295s (+3.9%)0.396s (-58.0% 🟢)4.190s (-8.4% 🟢)1.276s101.03x

🔍 Observability: Next.js (Turbopack) | Nitro | Express

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalNext.js (Turbopack)6/8
🐘 PostgresNext.js (Turbopack)5/8
▲ VercelNitro4/8
Fastest World by Framework

Winner determined by most benchmark wins

Framework🥇 Fastest WorldWins
Express💻 Local8/8
Next.js (Turbopack)🌐 Starter6/8
Nitro💻 Local8/8
Column Definitions
  • Workflow Time: Runtime reported by workflow (completedAt - createdAt) - primary metric
  • TTFB: Time to First Byte - time from workflow start until first stream byte received (stream benchmarks only)
  • Slurp: Time from first byte to complete stream consumption (stream benchmarks only)
  • Wall Time: Total testbench time (trigger workflow + poll for result)
  • Overhead: Testbench overhead (Wall Time - Workflow Time)
  • Samples: Number of benchmark iterations run
  • vs Fastest: How much slower compared to the fastest configuration for this benchmark

Worlds:

  • 💻 Local: In-memory filesystem world (local development)
  • 🐘 Postgres: PostgreSQL database world (local development)
  • ▲ Vercel: Vercel production/preview deployment
  • 🌐 Starter: Community world (local development)
  • 🌐 Turso: Community world (local development)
  • 🌐 MongoDB: Community world (local development)
  • 🌐 Redis: Community world (local development)
  • 🌐 Jazz: Community world (local development)

📋 View full workflow run

@VaguelySeriousVaguelySerious left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM ✨

One styling change:

className="inline-flex flex-col rounded text-[11px] font-mono my-1"
style={{
backgroundColor: colors.body,
border: `1px solid ${colors.header}`,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This leaves some weird extra white pixel borders

Suggested change
border: `1px solid ${colors.header}`,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually I tested too early, this seems to already be resolved

@socket-security

socket-securityBot commented Jan 20, 2026

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

DiffPackageSupply Chain
Security
VulnerabilityQualityMaintenanceLicense
Addednpm/​@​types/​color-hash@​2.0.01001009080100
Addednpm/​color-hash@​2.0.210010010080100

View full report

Comment thread.changeset/forty-tables-lick.md Outdated
TooTallNateand others added 2 commits January 20, 2026 01:39
Co-authored-by: Peter Wielander <mittgfu@gmail.com>
Signed-off-by: Nathan Rajlich <n@n8.io>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@TooTallNate@VaguelySerious
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Show opaque marker when deserializing custom classes and instances in o11y by TooTallNate · Pull Request #809 · vercel/workflow · GitHub
Skip to content

Show opaque marker when deserializing custom classes and instances in o11y - #809

Merged
TooTallNate merged 8 commits into
mainfrom
01-19-show_opaque_marker_when_deserializing_custom_classes_and_instances_in_o11y
Jan 20, 2026
Merged

Show opaque marker when deserializing custom classes and instances in o11y#809
TooTallNate merged 8 commits into
mainfrom
01-19-show_opaque_marker_when_deserializing_custom_classes_and_instances_in_o11y

Conversation

@TooTallNate

@TooTallNateTooTallNate commented Jan 19, 2026

Copy link
Copy Markdown
Member

Web example:

dark mode
Screenshot 2026-01-20 at 01 38 04

light mode
Screenshot 2026-01-20 at 01 38 09

CLI example:

Screenshot 2026-01-20 at 01 44 53

Added support for displaying custom class instances in the observability UI by showing opaque markers when deserializing custom classes.

What changed?

  • Added a CLASS_INSTANCE_REF_TYPE marker and related interfaces to represent serialized class instances that cannot be fully deserialized
  • Implemented helper functions to extract class names from class IDs and convert serialized instances to reference objects
  • Enhanced the stream print revivers to handle custom class instances and class references
  • Updated the attribute panel to recognize and display class instance references with a __class__ marker alongside the serialized data

How to test?

  1. Create a workflow that uses custom classes
  2. Run the workflow and view it in the observability UI
  3. Verify that custom class instances are displayed with their class names and serialized data
  4. Check that class references are shown as <class:ClassName> in the UI

Why make this change?

Previously, custom class instances couldn't be properly displayed in the observability UI because they weren't registered for deserialization in the o11y context. This change improves the developer experience by providing meaningful representations of custom classes and instances, making it easier to debug and understand workflow execution without requiring class registration.

@changeset-bot

changeset-botBot commented Jan 19, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: c889dd1

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 13 packages
NameType
@workflow/web-sharedPatch
@workflow/corePatch
@workflow/buildersPatch
@workflow/cliPatch
@workflow/docs-typecheckPatch
@workflow/nextPatch
@workflow/nitroPatch
workflowPatch
@workflow/astroPatch
@workflow/sveltekitPatch
@workflow/world-testingPatch
@workflow/nuxtPatch
@workflow/aiPatch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions

github-actionsBot commented Jan 19, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production435038473
❌ 💻 Local Development396232430
✅ 📦 Local Production398032430
✅ 🐘 Local Postgres398032430
✅ 🪟 Windows430043
❌ 🌍 Community Worlds162220184
Total1832241341990

❌ Failed Tests

💻 Local Development (2 failed)

sveltekit-stable (2 failed):

  • error handling error propagation step errors basic step error preserves message and stack trace
  • error handling error propagation step errors cross-file step error preserves message and function names in stack
🌍 Community Worlds (22 failed)

mongodb (1 failed):

  • webhookWorkflow

redis (1 failed):

  • webhookWorkflow

starter (19 failed):

  • addTenWorkflow
  • addTenWorkflow
  • error handling error propagation workflow errors nested function calls preserve message and stack trace
  • error handling error propagation workflow errors cross-file imports preserve message and stack trace
  • error handling error propagation step errors basic step error preserves message and stack trace
  • error handling error propagation step errors cross-file step error preserves message and function names in stack
  • error handling retry behavior regular Error retries until success
  • error handling retry behavior FatalError fails immediately without retries
  • error handling catchability FatalError can be caught and detected with FatalError.is()
  • hookCleanupTestWorkflow - hook token reuse after workflow completion
  • stepFunctionPassingWorkflow - step function references can be passed as arguments (without closure vars)
  • stepFunctionWithClosureWorkflow - step function with closure variables passed as argument
  • spawnWorkflowFromStepWorkflow - spawning a child workflow using start() inside a step
  • pathsAliasWorkflow - TypeScript path aliases resolve correctly
  • Calculator.calculate - static workflow method using static step methods from another class
  • AllInOneService.processNumber - static workflow method using sibling static step methods
  • ChainableService.processWithThis - static step methods using this to reference the class
  • thisSerializationWorkflow - step function invoked with .call() and .apply()
  • customSerializationWorkflow - custom class serialization with WORKFLOW_SERIALIZE/WORKFLOW_DESERIALIZE

turso (1 failed):

  • webhookWorkflow

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro3904
✅ example3904
✅ express3904
✅ fastify3904
✅ hono3904
✅ nextjs-turbopack4201
✅ nextjs-webpack4201
✅ nitro3904
✅ nuxt3904
✅ sveltekit3904
✅ vite3904
❌ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable3904
✅ express-stable3904
✅ fastify-stable3904
✅ hono-stable3904
✅ nextjs-turbopack-stable4300
✅ nextjs-webpack-stable4300
✅ nitro-stable3904
✅ nuxt-stable3904
❌ sveltekit-stable3724
✅ vite-stable3904
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable3904
✅ express-stable3904
✅ fastify-stable3904
✅ hono-stable3904
✅ nextjs-turbopack-stable4300
✅ nextjs-webpack-stable4300
✅ nitro-stable3904
✅ nuxt-stable3904
✅ sveltekit-stable3904
✅ vite-stable3904
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable3904
✅ express-stable3904
✅ fastify-stable3904
✅ hono-stable3904
✅ nextjs-turbopack-stable4300
✅ nextjs-webpack-stable4300
✅ nitro-stable3904
✅ nuxt-stable3904
✅ sveltekit-stable3904
✅ vite-stable3904
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack4300
❌ 🌍 Community Worlds
AppPassedFailedSkipped
✅ mongodb-dev300
❌ mongodb4210
✅ redis-dev300
❌ redis4210
✅ starter-dev300
❌ starter24190
✅ turso-dev300
❌ turso4210

📋 View full workflow run


Some E2E test jobs failed:

  • Vercel Prod: success
  • Local Dev: failure
  • Local Prod: success
  • Local Postgres: success
  • Windows: success

Check the workflow run for details.

@TooTallNateGraphite App

TooTallNate commented Jan 19, 2026

Copy link
Copy Markdown
MemberAuthor

@TooTallNate
TooTallNate marked this pull request as ready for review January 19, 2026 22:17
CopilotAI review requested due to automatic review settings January 19, 2026 22:17
@vercel

vercelBot commented Jan 19, 2026

Copy link
Copy Markdown
Contributor

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR enhances the observability UI to display custom class instances that cannot be fully deserialized because their classes are not registered in the o11y context. The implementation adds opaque markers for both class instances and class references, making it easier to debug workflows that use custom classes.

Changes:

  • Added CLASS_INSTANCE_REF_TYPE marker, ClassInstanceRef interface, and type guards to represent unregistered class instances
  • Implemented helper functions (extractClassName, serializedInstanceToRef, serializedClassToString) to convert serialized class data to display-friendly formats
  • Extended streamPrintRevivers to handle Instance and Class types by converting them to opaque markers
  • Updated UI transformation logic to display class instances inline with a __class__ marker alongside their serialized data

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 8 comments.

FileDescription
packages/core/src/observability.tsAdds ClassInstanceRef types, helper functions for class name extraction, and stream print revivers for Instance and Class types
packages/web-shared/src/sidebar/attribute-panel.tsxDuplicates ClassInstanceRef types for client-side use and updates transformValueForDisplay to handle class instance refs inline with class marker
.changeset/forty-tables-lick.mdDocuments the patch-level changes for both packages

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

'__type' in value &&
value.__type === CLASS_INSTANCE_REF_TYPE &&
'className' in value &&
typeof value.className === 'string'

CopilotAIJan 19, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The type guard isClassInstanceRef validates className but not classId. For consistency with isStreamRef which validates all required fields, and to ensure type safety, the function should also validate that classId exists and is a string.

Suggested change
typeofvalue.className==='string'
typeofvalue.className==='string'&&
'classId'invalue&&
typeofvalue.classId==='string'

Copilot uses AI. Check for mistakes.
Comment on lines +124 to +128
const extractClassName = (classId: string): string => {
if (!classId) return 'Unknown';
const parts = classId.split('/');
return parts[parts.length - 1] || classId;
};

CopilotAIJan 19, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The extractClassName function returns 'Unknown' when classId is falsy, but an empty string is falsy in JavaScript. When classId is an empty string, the function would return 'Unknown' without trying to split it. However, if the split produces an empty array or the last part is an empty string, it would fall back to returning the original classId. Consider handling the empty string case explicitly before the split operation to ensure consistent behavior.

Copilot uses AI. Check for mistakes.
Comment on lines +244 to +249
return {
__class__: v.className,
...(typeof v.data === 'object' && v.data !== null
? v.data
: { value: v.data }),
};

CopilotAIJan 19, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The spread operator on line 247 could potentially overwrite the class property if v.data is an object that contains a class key. This would result in the class name information being lost. Consider using a different marker name that's less likely to collide with actual object properties, or ensure the class property cannot be overwritten by spreading v.data after it instead of before.

Suggested change
return{
__class__: v.className,
...(typeofv.data==='object'&&v.data!==null
? v.data
: {value: v.data}),
};
returntypeofv.data==='object'&&v.data!==null
? {
...(v.dataasRecord<string,unknown>),
__class__: v.className,
}
: {
value: v.data,
__class__: v.className,
};

Copilot uses AI. Check for mistakes.
Comment on lines +244 to +248
return {
__class__: v.className,
...(typeof v.data === 'object' && v.data !== null
? v.data
: { value: v.data }),

CopilotAIJan 19, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The v.data object is spread directly without being transformed recursively. If v.data contains nested StreamRef or ClassInstanceRef objects, they won't be properly handled. The nested data should be recursively transformed before spreading to ensure all special references are properly converted.

Suggested change
return{
__class__: v.className,
...(typeofv.data==='object'&&v.data!==null
? v.data
: {value: v.data}),
consttransformedData=
typeofv.data==='object'&&v.data!==null
? transform(v.data)
: transform(v.data);
return{
__class__: v.className,
...(typeofv.data==='object'&&v.data!==null
? (transformedDataasRecord<string,unknown>)
: {value: transformedData}),

Copilot uses AI. Check for mistakes.
Comment on lines +151 to 157
const serializedClassToString = (value: { classId: string }): string => {
const className = extractClassName(value.classId);
return `<class:${className}>`;
};

/**
* This is an extra reviver for devalue that takes any streams that would be converted,

CopilotAIJan 19, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The serializedClassToString function doesn't validate that the input value has the expected structure. If value.classId is undefined or not a string, extractClassName will handle it, but it would be clearer to add explicit validation to ensure the function is called with the expected shape of data.

Suggested change
constserializedClassToString=(value: {classId: string}): string=>{
constclassName=extractClassName(value.classId);
return`<class:${className}>`;
};
/**
*Thisisanextrareviverfordevaluethattakesanystreamsthatwouldbeconverted,
*Convertaserializedclassreferencetoa string representation.
*ThisisusedforClasstype(theconstructorreferenceitself,notaninstance).
*/
constserializedClassToString=(value: unknown): string=>{
if(
!value||
typeofvalue!=='object'||
!('classId'invalue)||
typeof(valueas{classId: unknown}).classId!=='string'
){
return'<class:Unknown>';
}
constclassId=(valueas{classId: string}).classId;
constclassName=extractClassName(classId);
return`<class:${className}>`;
};

Copilot uses AI. Check for mistakes.
Comment threadpackages/core/src/observability.ts Outdated
Comment on lines +139 to +143
return {
__type: CLASS_INSTANCE_REF_TYPE,
className: extractClassName(value.classId),
classId: value.classId,
data: value.data,

CopilotAIJan 19, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The serializedInstanceToRef function doesn't validate that the input value has the expected structure. While the type annotation suggests the expected shape, adding runtime validation would make the function more robust, especially since it's called from the streamPrintRevivers which receives values of type any.

Suggested change
return{
__type: CLASS_INSTANCE_REF_TYPE,
className: extractClassName(value.classId),
classId: value.classId,
data: value.data,
// Runtime validation because this function is ultimately called with values of type `any`.
constisObject=value!==null&&typeofvalue==='object';
constclassId=isObject&&typeof(valueasany).classId==='string'
? (valueasany).classId
: '';
constdata=isObject ? (valueasany).data : value;
return{
__type: CLASS_INSTANCE_REF_TYPE,
className: extractClassName(classId),
classId,
data,

Copilot uses AI. Check for mistakes.
Comment on lines +120 to +154
/**
* Extract the class name from a classId.
* The classId format is typically "path/to/file/ClassName" so we extract the last segment.
*/
const extractClassName = (classId: string): string => {
if (!classId) return 'Unknown';
const parts = classId.split('/');
return parts[parts.length - 1] || classId;
};

/**
* Convert a serialized class instance to a ClassInstanceRef for o11y display.
* This allows viewing custom class instances in the UI without needing
* the class to be registered for deserialization.
*/
const serializedInstanceToRef = (value: {
classId: string;
data: unknown;
}): ClassInstanceRef => {
return {
__type: CLASS_INSTANCE_REF_TYPE,
className: extractClassName(value.classId),
classId: value.classId,
data: value.data,
};
};

/**
* Convert a serialized class reference to a string representation.
* This is used for Class type (the constructor reference itself, not an instance).
*/
const serializedClassToString = (value: { classId: string }): string => {
const className = extractClassName(value.classId);
return `<class:${className}>`;
};

CopilotAIJan 19, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new helper functions extractClassName, serializedInstanceToRef, and serializedClassToString lack test coverage. Given that other utility functions in the codebase have comprehensive test files (e.g., serialization.test.ts, parse-name.test.ts), consider adding tests to verify the behavior of these functions, especially edge cases like empty classId, classIds with different path separators, and handling of various data types.

Copilot uses AI. Check for mistakes.
Comment threadpackages/core/src/observability.ts Outdated
'__type' in value &&
value.__type === CLASS_INSTANCE_REF_TYPE &&
'className' in value &&
typeof value.className === 'string'

CopilotAIJan 19, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The type guard isClassInstanceRef validates className but not classId. For consistency with isStreamRef which validates all required fields, and to ensure type safety, the function should also validate that classId exists and is a string. This would prevent runtime errors if a malformed ClassInstanceRef object is encountered.

Suggested change
typeofvalue.className==='string'
typeofvalue.className==='string'&&
'classId'invalue&&
typeofvalue.classId==='string'

Copilot uses AI. Check for mistakes.
@TooTallNate
TooTallNateforce-pushed the 01-19-show_opaque_marker_when_deserializing_custom_classes_and_instances_in_o11y branch from 127cfec to c46327fCompareJanuary 19, 2026 22:26
@TooTallNate
TooTallNateforce-pushed the 01-09-add_support_for_custom_class_instance_serialization branch from baea85a to 4216347CompareJanuary 19, 2026 22:26
value: unknown
): { json: string; streamRefs: Map<string, StreamRef> } => {
): {
json: string;

@vercelvercelBotJan 19, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ClassInstanceRef transformation allows properties in v.data to overwrite the class marker, and doesn't recursively transform nested ClassInstanceRef objects within v.data

Fix on Vercel

* Used in o11y when a custom class instance is encountered but the class is not
* registered for deserialization.
*/
interface ClassInstanceRef {

@vercelvercelBotJan 19, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Type guard isClassInstanceRef doesn't validate classId and data properties required by ClassInstanceRef interface, leading to type unsafety

Fix on Vercel

Comment threadpackages/core/src/observability.ts Outdated
'__type' in value &&
value.__type === CLASS_INSTANCE_REF_TYPE &&
'className' in value &&
typeof value.className === 'string'

@vercelvercelBotJan 19, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The isClassInstanceRef type guard doesn't validate classId and data properties required by the ClassInstanceRef interface

Fix on Vercel

@TooTallNate
TooTallNateforce-pushed the 01-19-show_opaque_marker_when_deserializing_custom_classes_and_instances_in_o11y branch from a14fe6d to 04b36f5CompareJanuary 19, 2026 23:14
@github-actions

github-actionsBot commented Jan 19, 2026

Copy link
Copy Markdown
Contributor

📊 Benchmark Results

📈 Comparing against baseline from main branch. Green 🟢 = faster, Red 🔺 = slower.

workflow with no steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Starter🥇 Next.js (Turbopack)0.040s (-1.2%)1.014s (~)0.974s101.00x
💻 LocalNext.js (Turbopack)0.042s (+2.7%)1.019s (~)0.977s101.06x
💻 LocalNitro0.043s (-3.3%)1.007s (~)0.964s101.09x
💻 LocalExpress0.044s (-1.1%)1.007s (~)0.963s101.11x
🌐 RedisNext.js (Turbopack)0.045s (+11.9% 🔺)1.018s (~)0.973s101.14x
🌐 TursoNext.js (Turbopack)0.106s (-3.1%)1.013s (~)0.908s102.66x
🌐 MongoDBNext.js (Turbopack)0.122s (+123.6% 🔺)1.018s (~)0.895s103.08x
🐘 PostgresNext.js (Turbopack)0.176s (-44.7% 🟢)1.022s (~)0.845s104.44x
🐘 PostgresNitro0.337s (+6.8% 🔺)1.012s (~)0.675s108.49x
🐘 PostgresExpress0.338s (+5.0% 🔺)1.013s (~)0.675s108.52x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro0.575s (-80.7% 🟢)1.580s (-58.0% 🟢)1.005s101.00x
▲ VercelExpress0.689s (-83.2% 🟢)1.686s (-66.5% 🟢)0.997s101.20x
▲ VercelNext.js (Turbopack)0.744s (-66.1% 🟢)1.568s (-45.5% 🟢)0.824s101.29x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Starter🥇 Next.js (Turbopack)1.090s (~)2.010s (~)0.920s101.00x
💻 LocalNext.js (Turbopack)1.098s (+0.6%)2.012s (~)0.914s101.01x
💻 LocalExpress1.112s (~)2.006s (~)0.895s101.02x
🌐 RedisNext.js (Turbopack)1.113s (+0.8%)2.013s (~)0.900s101.02x
💻 LocalNitro1.115s (~)2.007s (~)0.892s101.02x
🌐 MongoDBNext.js (Turbopack)1.261s (-4.0%)2.016s (~)0.755s101.16x
🌐 TursoNext.js (Turbopack)1.305s (~)2.013s (~)0.707s101.20x
🐘 PostgresExpress2.185s (+1.0%)3.015s (~)0.830s102.00x
🐘 PostgresNitro2.194s (+0.7%)3.015s (~)0.821s102.01x
🐘 PostgresNext.js (Turbopack)2.322s (+35.2% 🔺)2.818s (+33.2% 🔺)0.496s102.13x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)2.682s (-67.8% 🟢)3.592s (-59.7% 🟢)0.910s101.00x
▲ VercelExpress2.694s (-60.3% 🟢)3.718s (-51.6% 🟢)1.024s101.00x
▲ VercelNitro2.712s (-61.9% 🟢)3.698s (-53.0% 🟢)0.987s101.01x

🔍 Observability: Next.js (Turbopack) | Express | Nitro

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Starter🥇 Next.js (Turbopack)10.606s (~)11.012s (~)0.406s51.00x
💻 LocalNext.js (Turbopack)10.664s (~)11.014s (~)0.350s51.01x
🌐 RedisNext.js (Turbopack)10.708s (~)11.021s (~)0.314s51.01x
💻 LocalExpress10.787s (~)11.010s (~)0.223s51.02x
💻 LocalNitro10.789s (~)11.009s (~)0.220s51.02x
🌐 MongoDBNext.js (Turbopack)12.119s (-1.1%)12.829s (-1.5%)0.710s51.14x
🌐 TursoNext.js (Turbopack)12.175s (~)13.020s (~)0.845s51.15x
🐘 PostgresNext.js (Turbopack)15.410s (+2.7%)15.838s (+1.3%)0.428s51.45x
🐘 PostgresNitro20.351s (~)21.029s (~)0.678s51.92x
🐘 PostgresExpress20.416s (~)21.034s (~)0.618s51.92x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express21.836s (-1.2%)22.549s (-1.4%)0.713s51.00x
▲ VercelNext.js (Turbopack)22.144s (-0.7%)22.815s (~)0.671s51.01x
▲ VercelNitro22.426s (+2.0%)23.192s (+1.9%)0.767s51.03x

🔍 Observability: Express | Next.js (Turbopack) | Nitro

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Starter🥇 Next.js (Turbopack)1.355s (~)2.008s (~)0.653s151.00x
🌐 RedisNext.js (Turbopack)1.374s (+1.7%)2.011s (~)0.637s151.01x
💻 LocalNext.js (Turbopack)1.392s (~)2.013s (~)0.621s151.03x
💻 LocalNitro1.417s (+0.9%)2.005s (~)0.588s151.05x
💻 LocalExpress1.420s (+0.6%)2.007s (~)0.587s151.05x
🐘 PostgresNext.js (Turbopack)1.877s (+5.9% 🔺)2.014s (~)0.136s151.39x
🌐 MongoDBNext.js (Turbopack)2.144s (+1.0%)3.014s (~)0.870s101.58x
🌐 TursoNext.js (Turbopack)2.222s (+1.3%)3.012s (~)0.790s101.64x
🐘 PostgresExpress2.363s (~)3.012s (~)0.649s101.74x
🐘 PostgresNitro2.487s (+3.1%)3.012s (~)0.525s101.84x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.936s (-8.1% 🟢)3.789s (-5.6% 🟢)0.853s81.00x
▲ VercelExpress3.069s (+6.4% 🔺)3.925s (+7.8% 🔺)0.856s81.05x
▲ VercelNext.js (Turbopack)3.214s (+7.1% 🔺)3.982s (+6.0% 🔺)0.768s81.09x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)2.040s (-1.4%)2.896s (+3.9%)0.855s111.00x
💻 LocalExpress2.208s (~)3.181s (~)0.973s101.08x
💻 LocalNitro2.241s (+1.4%)3.168s (~)0.928s101.10x
🌐 StarterNext.js (Turbopack)2.483s (~)3.008s (~)0.525s101.22x
🌐 RedisNext.js (Turbopack)2.492s (+0.6%)3.012s (-0.7%)0.520s101.22x
🐘 PostgresNext.js (Turbopack)2.550s (-5.2% 🟢)3.020s (-0.8%)0.470s101.25x
🐘 PostgresExpress2.876s (-6.9% 🟢)3.232s (-17.3% 🟢)0.356s101.41x
🐘 PostgresNitro2.955s (~)3.114s (-7.0% 🟢)0.159s101.45x
🌐 MongoDBNext.js (Turbopack)4.650s (-0.6%)5.185s (~)0.535s62.28x
🌐 TursoNext.js (Turbopack)4.851s (+1.7%)5.180s (~)0.329s62.38x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.997s (+11.6% 🔺)4.820s (+16.3% 🔺)0.823s71.00x
▲ VercelExpress4.022s (+28.6% 🔺)4.751s (+23.7% 🔺)0.729s71.01x
▲ VercelNext.js (Turbopack)4.660s (+40.1% 🔺)5.415s (+38.1% 🔺)0.755s61.17x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Starter🥇 Next.js (Turbopack)1.359s (+0.5%)2.007s (~)0.648s151.00x
🌐 RedisNext.js (Turbopack)1.367s (~)2.011s (~)0.644s151.01x
💻 LocalExpress1.406s (-1.4%)2.006s (~)0.600s151.03x
💻 LocalNext.js (Turbopack)1.409s (+1.3%)2.015s (~)0.605s151.04x
💻 LocalNitro1.428s (~)2.005s (~)0.577s151.05x
🐘 PostgresNitro1.557s (-23.4% 🟢)2.010s (-16.1% 🟢)0.452s151.15x
🐘 PostgresNext.js (Turbopack)1.676s (-25.9% 🟢)2.013s (-25.2% 🟢)0.337s151.23x
🐘 PostgresExpress1.836s (+3.1%)2.010s (-3.3%)0.174s151.35x
🌐 MongoDBNext.js (Turbopack)2.116s (-0.9%)3.019s (~)0.903s101.56x
🌐 TursoNext.js (Turbopack)2.219s (-1.2%)3.013s (~)0.794s101.63x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)2.837s (+2.4%)3.571s (-2.7%)0.735s91.00x
▲ VercelExpress3.139s (+7.3% 🔺)4.119s (+10.8% 🔺)0.980s81.11x
▲ VercelNitro3.218s (-5.2% 🟢)4.497s (+2.9%)1.279s71.13x

🔍 Observability: Next.js (Turbopack) | Express | Nitro

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express2.235s (+0.7%)3.197s (+0.6%)0.963s101.00x
💻 LocalNext.js (Turbopack)2.307s (+8.1% 🔺)3.142s (+3.7%)0.835s101.03x
💻 LocalNitro2.378s (+3.5%)3.317s (+2.9%)0.939s101.06x
🌐 StarterNext.js (Turbopack)2.459s (~)3.008s (~)0.549s101.10x
🐘 PostgresNitro2.497s (-18.3% 🟢)3.017s (-15.7% 🟢)0.520s101.12x
🌐 RedisNext.js (Turbopack)2.517s (+1.3%)3.012s (~)0.495s101.13x
🐘 PostgresNext.js (Turbopack)2.583s (-5.0%)3.029s (-0.8%)0.447s101.16x
🐘 PostgresExpress2.642s (-8.9% 🟢)3.018s (-6.5% 🟢)0.377s101.18x
🌐 MongoDBNext.js (Turbopack)4.711s (~)5.181s (~)0.470s62.11x
🌐 TursoNext.js (Turbopack)4.778s (+1.6%)5.183s (-3.1%)0.405s62.14x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.209s (+2.5%)3.778s (+0.8%)0.569s81.00x
▲ VercelExpress3.254s (~)3.848s (+2.4%)0.594s81.01x
▲ VercelNext.js (Turbopack)3.454s (+0.9%)4.032s (~)0.578s81.08x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🌐 Starter🥇 Next.js (Turbopack)0.129s (~)1.005s (~)0.000s (NaN%)1.011s (~)0.882s101.00x
💻 LocalNext.js (Turbopack)0.141s (-3.4%)1.003s (~)0.017s (-0.6%)1.028s (~)0.887s101.09x
🌐 RedisNext.js (Turbopack)0.159s (+11.6% 🔺)1.005s (~)0.000s (-100.0% 🟢)1.015s (~)0.855s101.23x
💻 LocalExpress0.176s (-0.7%)0.992s (~)0.015s (-1.9%)1.022s (~)0.846s101.36x
💻 LocalNitro0.182s (+1.7%)0.992s (~)0.015s (-0.7%)1.021s (~)0.838s101.41x
🌐 TursoNext.js (Turbopack)0.488s (+6.3% 🔺)0.965s (-3.0%)0.000s (+Infinity% 🔺)1.013s (~)0.524s103.78x
🌐 MongoDBNext.js (Turbopack)0.506s (-3.3%)0.941s (+1.7%)0.000s (+Infinity% 🔺)1.015s (~)0.509s103.92x
🐘 PostgresNext.js (Turbopack)0.673s (-29.9% 🟢)1.052s (-1.5%)0.000s (-100.0% 🟢)1.118s (-15.1% 🟢)0.444s105.21x
🐘 PostgresExpress2.313s (+3.0%)2.728s (-2.7%)0.000s (-100.0% 🟢)3.014s (~)0.700s1017.90x
🐘 PostgresNitro2.402s (+3.2%)2.639s (-2.9%)0.000s (+Infinity% 🔺)3.014s (~)0.611s1018.59x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)2.826s (~)3.323s (+6.2% 🔺)0.378s (-49.5% 🟢)4.114s (-4.7%)1.288s101.00x
▲ VercelNitro2.891s (-0.7%)3.301s (+3.4%)0.454s (-23.0% 🟢)4.226s (-0.6%)1.335s101.02x
▲ VercelExpress2.914s (+1.5%)3.295s (+3.9%)0.396s (-58.0% 🟢)4.190s (-8.4% 🟢)1.276s101.03x

🔍 Observability: Next.js (Turbopack) | Nitro | Express

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalNext.js (Turbopack)6/8
🐘 PostgresNext.js (Turbopack)5/8
▲ VercelNitro4/8
Fastest World by Framework

Winner determined by most benchmark wins

Framework🥇 Fastest WorldWins
Express💻 Local8/8
Next.js (Turbopack)🌐 Starter6/8
Nitro💻 Local8/8
Column Definitions
  • Workflow Time: Runtime reported by workflow (completedAt - createdAt) - primary metric
  • TTFB: Time to First Byte - time from workflow start until first stream byte received (stream benchmarks only)
  • Slurp: Time from first byte to complete stream consumption (stream benchmarks only)
  • Wall Time: Total testbench time (trigger workflow + poll for result)
  • Overhead: Testbench overhead (Wall Time - Workflow Time)
  • Samples: Number of benchmark iterations run
  • vs Fastest: How much slower compared to the fastest configuration for this benchmark

Worlds:

  • 💻 Local: In-memory filesystem world (local development)
  • 🐘 Postgres: PostgreSQL database world (local development)
  • ▲ Vercel: Vercel production/preview deployment
  • 🌐 Starter: Community world (local development)
  • 🌐 Turso: Community world (local development)
  • 🌐 MongoDB: Community world (local development)
  • 🌐 Redis: Community world (local development)
  • 🌐 Jazz: Community world (local development)

📋 View full workflow run

@VaguelySeriousVaguelySerious left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM ✨

One styling change:

className="inline-flex flex-col rounded text-[11px] font-mono my-1"
style={{
backgroundColor: colors.body,
border: `1px solid ${colors.header}`,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This leaves some weird extra white pixel borders

Suggested change
border: `1px solid ${colors.header}`,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually I tested too early, this seems to already be resolved

@socket-security

socket-securityBot commented Jan 20, 2026

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

DiffPackageSupply Chain
Security
VulnerabilityQualityMaintenanceLicense
Addednpm/​@​types/​color-hash@​2.0.01001009080100
Addednpm/​color-hash@​2.0.210010010080100

View full report

Comment thread.changeset/forty-tables-lick.md Outdated
TooTallNateand others added 2 commits January 20, 2026 01:39
Co-authored-by: Peter Wielander <mittgfu@gmail.com>
Signed-off-by: Nathan Rajlich <n@n8.io>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@TooTallNate@VaguelySerious
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' Show opaque marker when deserializing custom classes and instances in o11y by TooTallNate · Pull Request #809 · vercel/workflow · GitHub
Skip to content

Show opaque marker when deserializing custom classes and instances in o11y - #809

Merged
TooTallNate merged 8 commits into
mainfrom
01-19-show_opaque_marker_when_deserializing_custom_classes_and_instances_in_o11y
Jan 20, 2026
Merged

Show opaque marker when deserializing custom classes and instances in o11y#809
TooTallNate merged 8 commits into
mainfrom
01-19-show_opaque_marker_when_deserializing_custom_classes_and_instances_in_o11y

Conversation

@TooTallNate

@TooTallNateTooTallNate commented Jan 19, 2026

Copy link
Copy Markdown
Member

Web example:

dark mode
Screenshot 2026-01-20 at 01 38 04

light mode
Screenshot 2026-01-20 at 01 38 09

CLI example:

Screenshot 2026-01-20 at 01 44 53

Added support for displaying custom class instances in the observability UI by showing opaque markers when deserializing custom classes.

What changed?

  • Added a CLASS_INSTANCE_REF_TYPE marker and related interfaces to represent serialized class instances that cannot be fully deserialized
  • Implemented helper functions to extract class names from class IDs and convert serialized instances to reference objects
  • Enhanced the stream print revivers to handle custom class instances and class references
  • Updated the attribute panel to recognize and display class instance references with a __class__ marker alongside the serialized data

How to test?

  1. Create a workflow that uses custom classes
  2. Run the workflow and view it in the observability UI
  3. Verify that custom class instances are displayed with their class names and serialized data
  4. Check that class references are shown as <class:ClassName> in the UI

Why make this change?

Previously, custom class instances couldn't be properly displayed in the observability UI because they weren't registered for deserialization in the o11y context. This change improves the developer experience by providing meaningful representations of custom classes and instances, making it easier to debug and understand workflow execution without requiring class registration.

@changeset-bot

changeset-botBot commented Jan 19, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: c889dd1

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 13 packages
NameType
@workflow/web-sharedPatch
@workflow/corePatch
@workflow/buildersPatch
@workflow/cliPatch
@workflow/docs-typecheckPatch
@workflow/nextPatch
@workflow/nitroPatch
workflowPatch
@workflow/astroPatch
@workflow/sveltekitPatch
@workflow/world-testingPatch
@workflow/nuxtPatch
@workflow/aiPatch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions

github-actionsBot commented Jan 19, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production435038473
❌ 💻 Local Development396232430
✅ 📦 Local Production398032430
✅ 🐘 Local Postgres398032430
✅ 🪟 Windows430043
❌ 🌍 Community Worlds162220184
Total1832241341990

❌ Failed Tests

💻 Local Development (2 failed)

sveltekit-stable (2 failed):

  • error handling error propagation step errors basic step error preserves message and stack trace
  • error handling error propagation step errors cross-file step error preserves message and function names in stack
🌍 Community Worlds (22 failed)

mongodb (1 failed):

  • webhookWorkflow

redis (1 failed):

  • webhookWorkflow

starter (19 failed):

  • addTenWorkflow
  • addTenWorkflow
  • error handling error propagation workflow errors nested function calls preserve message and stack trace
  • error handling error propagation workflow errors cross-file imports preserve message and stack trace
  • error handling error propagation step errors basic step error preserves message and stack trace
  • error handling error propagation step errors cross-file step error preserves message and function names in stack
  • error handling retry behavior regular Error retries until success
  • error handling retry behavior FatalError fails immediately without retries
  • error handling catchability FatalError can be caught and detected with FatalError.is()
  • hookCleanupTestWorkflow - hook token reuse after workflow completion
  • stepFunctionPassingWorkflow - step function references can be passed as arguments (without closure vars)
  • stepFunctionWithClosureWorkflow - step function with closure variables passed as argument
  • spawnWorkflowFromStepWorkflow - spawning a child workflow using start() inside a step
  • pathsAliasWorkflow - TypeScript path aliases resolve correctly
  • Calculator.calculate - static workflow method using static step methods from another class
  • AllInOneService.processNumber - static workflow method using sibling static step methods
  • ChainableService.processWithThis - static step methods using this to reference the class
  • thisSerializationWorkflow - step function invoked with .call() and .apply()
  • customSerializationWorkflow - custom class serialization with WORKFLOW_SERIALIZE/WORKFLOW_DESERIALIZE

turso (1 failed):

  • webhookWorkflow

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro3904
✅ example3904
✅ express3904
✅ fastify3904
✅ hono3904
✅ nextjs-turbopack4201
✅ nextjs-webpack4201
✅ nitro3904
✅ nuxt3904
✅ sveltekit3904
✅ vite3904
❌ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable3904
✅ express-stable3904
✅ fastify-stable3904
✅ hono-stable3904
✅ nextjs-turbopack-stable4300
✅ nextjs-webpack-stable4300
✅ nitro-stable3904
✅ nuxt-stable3904
❌ sveltekit-stable3724
✅ vite-stable3904
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable3904
✅ express-stable3904
✅ fastify-stable3904
✅ hono-stable3904
✅ nextjs-turbopack-stable4300
✅ nextjs-webpack-stable4300
✅ nitro-stable3904
✅ nuxt-stable3904
✅ sveltekit-stable3904
✅ vite-stable3904
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable3904
✅ express-stable3904
✅ fastify-stable3904
✅ hono-stable3904
✅ nextjs-turbopack-stable4300
✅ nextjs-webpack-stable4300
✅ nitro-stable3904
✅ nuxt-stable3904
✅ sveltekit-stable3904
✅ vite-stable3904
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack4300
❌ 🌍 Community Worlds
AppPassedFailedSkipped
✅ mongodb-dev300
❌ mongodb4210
✅ redis-dev300
❌ redis4210
✅ starter-dev300
❌ starter24190
✅ turso-dev300
❌ turso4210

📋 View full workflow run


Some E2E test jobs failed:

  • Vercel Prod: success
  • Local Dev: failure
  • Local Prod: success
  • Local Postgres: success
  • Windows: success

Check the workflow run for details.

@TooTallNateGraphite App

TooTallNate commented Jan 19, 2026

Copy link
Copy Markdown
MemberAuthor

@TooTallNate
TooTallNate marked this pull request as ready for review January 19, 2026 22:17
CopilotAI review requested due to automatic review settings January 19, 2026 22:17
@vercel

vercelBot commented Jan 19, 2026

Copy link
Copy Markdown
Contributor

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR enhances the observability UI to display custom class instances that cannot be fully deserialized because their classes are not registered in the o11y context. The implementation adds opaque markers for both class instances and class references, making it easier to debug workflows that use custom classes.

Changes:

  • Added CLASS_INSTANCE_REF_TYPE marker, ClassInstanceRef interface, and type guards to represent unregistered class instances
  • Implemented helper functions (extractClassName, serializedInstanceToRef, serializedClassToString) to convert serialized class data to display-friendly formats
  • Extended streamPrintRevivers to handle Instance and Class types by converting them to opaque markers
  • Updated UI transformation logic to display class instances inline with a __class__ marker alongside their serialized data

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 8 comments.

FileDescription
packages/core/src/observability.tsAdds ClassInstanceRef types, helper functions for class name extraction, and stream print revivers for Instance and Class types
packages/web-shared/src/sidebar/attribute-panel.tsxDuplicates ClassInstanceRef types for client-side use and updates transformValueForDisplay to handle class instance refs inline with class marker
.changeset/forty-tables-lick.mdDocuments the patch-level changes for both packages

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

'__type' in value &&
value.__type === CLASS_INSTANCE_REF_TYPE &&
'className' in value &&
typeof value.className === 'string'

CopilotAIJan 19, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The type guard isClassInstanceRef validates className but not classId. For consistency with isStreamRef which validates all required fields, and to ensure type safety, the function should also validate that classId exists and is a string.

Suggested change
typeofvalue.className==='string'
typeofvalue.className==='string'&&
'classId'invalue&&
typeofvalue.classId==='string'

Copilot uses AI. Check for mistakes.
Comment on lines +124 to +128
const extractClassName = (classId: string): string => {
if (!classId) return 'Unknown';
const parts = classId.split('/');
return parts[parts.length - 1] || classId;
};

CopilotAIJan 19, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The extractClassName function returns 'Unknown' when classId is falsy, but an empty string is falsy in JavaScript. When classId is an empty string, the function would return 'Unknown' without trying to split it. However, if the split produces an empty array or the last part is an empty string, it would fall back to returning the original classId. Consider handling the empty string case explicitly before the split operation to ensure consistent behavior.

Copilot uses AI. Check for mistakes.
Comment on lines +244 to +249
return {
__class__: v.className,
...(typeof v.data === 'object' && v.data !== null
? v.data
: { value: v.data }),
};

CopilotAIJan 19, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The spread operator on line 247 could potentially overwrite the class property if v.data is an object that contains a class key. This would result in the class name information being lost. Consider using a different marker name that's less likely to collide with actual object properties, or ensure the class property cannot be overwritten by spreading v.data after it instead of before.

Suggested change
return{
__class__: v.className,
...(typeofv.data==='object'&&v.data!==null
? v.data
: {value: v.data}),
};
returntypeofv.data==='object'&&v.data!==null
? {
...(v.dataasRecord<string,unknown>),
__class__: v.className,
}
: {
value: v.data,
__class__: v.className,
};

Copilot uses AI. Check for mistakes.
Comment on lines +244 to +248
return {
__class__: v.className,
...(typeof v.data === 'object' && v.data !== null
? v.data
: { value: v.data }),

CopilotAIJan 19, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The v.data object is spread directly without being transformed recursively. If v.data contains nested StreamRef or ClassInstanceRef objects, they won't be properly handled. The nested data should be recursively transformed before spreading to ensure all special references are properly converted.

Suggested change
return{
__class__: v.className,
...(typeofv.data==='object'&&v.data!==null
? v.data
: {value: v.data}),
consttransformedData=
typeofv.data==='object'&&v.data!==null
? transform(v.data)
: transform(v.data);
return{
__class__: v.className,
...(typeofv.data==='object'&&v.data!==null
? (transformedDataasRecord<string,unknown>)
: {value: transformedData}),

Copilot uses AI. Check for mistakes.
Comment on lines +151 to 157
const serializedClassToString = (value: { classId: string }): string => {
const className = extractClassName(value.classId);
return `<class:${className}>`;
};

/**
* This is an extra reviver for devalue that takes any streams that would be converted,

CopilotAIJan 19, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The serializedClassToString function doesn't validate that the input value has the expected structure. If value.classId is undefined or not a string, extractClassName will handle it, but it would be clearer to add explicit validation to ensure the function is called with the expected shape of data.

Suggested change
constserializedClassToString=(value: {classId: string}): string=>{
constclassName=extractClassName(value.classId);
return`<class:${className}>`;
};
/**
*Thisisanextrareviverfordevaluethattakesanystreamsthatwouldbeconverted,
*Convertaserializedclassreferencetoa string representation.
*ThisisusedforClasstype(theconstructorreferenceitself,notaninstance).
*/
constserializedClassToString=(value: unknown): string=>{
if(
!value||
typeofvalue!=='object'||
!('classId'invalue)||
typeof(valueas{classId: unknown}).classId!=='string'
){
return'<class:Unknown>';
}
constclassId=(valueas{classId: string}).classId;
constclassName=extractClassName(classId);
return`<class:${className}>`;
};

Copilot uses AI. Check for mistakes.
Comment threadpackages/core/src/observability.ts Outdated
Comment on lines +139 to +143
return {
__type: CLASS_INSTANCE_REF_TYPE,
className: extractClassName(value.classId),
classId: value.classId,
data: value.data,

CopilotAIJan 19, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The serializedInstanceToRef function doesn't validate that the input value has the expected structure. While the type annotation suggests the expected shape, adding runtime validation would make the function more robust, especially since it's called from the streamPrintRevivers which receives values of type any.

Suggested change
return{
__type: CLASS_INSTANCE_REF_TYPE,
className: extractClassName(value.classId),
classId: value.classId,
data: value.data,
// Runtime validation because this function is ultimately called with values of type `any`.
constisObject=value!==null&&typeofvalue==='object';
constclassId=isObject&&typeof(valueasany).classId==='string'
? (valueasany).classId
: '';
constdata=isObject ? (valueasany).data : value;
return{
__type: CLASS_INSTANCE_REF_TYPE,
className: extractClassName(classId),
classId,
data,

Copilot uses AI. Check for mistakes.
Comment on lines +120 to +154
/**
* Extract the class name from a classId.
* The classId format is typically "path/to/file/ClassName" so we extract the last segment.
*/
const extractClassName = (classId: string): string => {
if (!classId) return 'Unknown';
const parts = classId.split('/');
return parts[parts.length - 1] || classId;
};

/**
* Convert a serialized class instance to a ClassInstanceRef for o11y display.
* This allows viewing custom class instances in the UI without needing
* the class to be registered for deserialization.
*/
const serializedInstanceToRef = (value: {
classId: string;
data: unknown;
}): ClassInstanceRef => {
return {
__type: CLASS_INSTANCE_REF_TYPE,
className: extractClassName(value.classId),
classId: value.classId,
data: value.data,
};
};

/**
* Convert a serialized class reference to a string representation.
* This is used for Class type (the constructor reference itself, not an instance).
*/
const serializedClassToString = (value: { classId: string }): string => {
const className = extractClassName(value.classId);
return `<class:${className}>`;
};

CopilotAIJan 19, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new helper functions extractClassName, serializedInstanceToRef, and serializedClassToString lack test coverage. Given that other utility functions in the codebase have comprehensive test files (e.g., serialization.test.ts, parse-name.test.ts), consider adding tests to verify the behavior of these functions, especially edge cases like empty classId, classIds with different path separators, and handling of various data types.

Copilot uses AI. Check for mistakes.
Comment threadpackages/core/src/observability.ts Outdated
'__type' in value &&
value.__type === CLASS_INSTANCE_REF_TYPE &&
'className' in value &&
typeof value.className === 'string'

CopilotAIJan 19, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The type guard isClassInstanceRef validates className but not classId. For consistency with isStreamRef which validates all required fields, and to ensure type safety, the function should also validate that classId exists and is a string. This would prevent runtime errors if a malformed ClassInstanceRef object is encountered.

Suggested change
typeofvalue.className==='string'
typeofvalue.className==='string'&&
'classId'invalue&&
typeofvalue.classId==='string'

Copilot uses AI. Check for mistakes.
@TooTallNate
TooTallNateforce-pushed the 01-19-show_opaque_marker_when_deserializing_custom_classes_and_instances_in_o11y branch from 127cfec to c46327fCompareJanuary 19, 2026 22:26
@TooTallNate
TooTallNateforce-pushed the 01-09-add_support_for_custom_class_instance_serialization branch from baea85a to 4216347CompareJanuary 19, 2026 22:26
value: unknown
): { json: string; streamRefs: Map<string, StreamRef> } => {
): {
json: string;

@vercelvercelBotJan 19, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ClassInstanceRef transformation allows properties in v.data to overwrite the class marker, and doesn't recursively transform nested ClassInstanceRef objects within v.data

Fix on Vercel

* Used in o11y when a custom class instance is encountered but the class is not
* registered for deserialization.
*/
interface ClassInstanceRef {

@vercelvercelBotJan 19, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Type guard isClassInstanceRef doesn't validate classId and data properties required by ClassInstanceRef interface, leading to type unsafety

Fix on Vercel

Comment threadpackages/core/src/observability.ts Outdated
'__type' in value &&
value.__type === CLASS_INSTANCE_REF_TYPE &&
'className' in value &&
typeof value.className === 'string'

@vercelvercelBotJan 19, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The isClassInstanceRef type guard doesn't validate classId and data properties required by the ClassInstanceRef interface

Fix on Vercel

@TooTallNate
TooTallNateforce-pushed the 01-19-show_opaque_marker_when_deserializing_custom_classes_and_instances_in_o11y branch from a14fe6d to 04b36f5CompareJanuary 19, 2026 23:14
@github-actions

github-actionsBot commented Jan 19, 2026

Copy link
Copy Markdown
Contributor

📊 Benchmark Results

📈 Comparing against baseline from main branch. Green 🟢 = faster, Red 🔺 = slower.

workflow with no steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Starter🥇 Next.js (Turbopack)0.040s (-1.2%)1.014s (~)0.974s101.00x
💻 LocalNext.js (Turbopack)0.042s (+2.7%)1.019s (~)0.977s101.06x
💻 LocalNitro0.043s (-3.3%)1.007s (~)0.964s101.09x
💻 LocalExpress0.044s (-1.1%)1.007s (~)0.963s101.11x
🌐 RedisNext.js (Turbopack)0.045s (+11.9% 🔺)1.018s (~)0.973s101.14x
🌐 TursoNext.js (Turbopack)0.106s (-3.1%)1.013s (~)0.908s102.66x
🌐 MongoDBNext.js (Turbopack)0.122s (+123.6% 🔺)1.018s (~)0.895s103.08x
🐘 PostgresNext.js (Turbopack)0.176s (-44.7% 🟢)1.022s (~)0.845s104.44x
🐘 PostgresNitro0.337s (+6.8% 🔺)1.012s (~)0.675s108.49x
🐘 PostgresExpress0.338s (+5.0% 🔺)1.013s (~)0.675s108.52x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro0.575s (-80.7% 🟢)1.580s (-58.0% 🟢)1.005s101.00x
▲ VercelExpress0.689s (-83.2% 🟢)1.686s (-66.5% 🟢)0.997s101.20x
▲ VercelNext.js (Turbopack)0.744s (-66.1% 🟢)1.568s (-45.5% 🟢)0.824s101.29x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Starter🥇 Next.js (Turbopack)1.090s (~)2.010s (~)0.920s101.00x
💻 LocalNext.js (Turbopack)1.098s (+0.6%)2.012s (~)0.914s101.01x
💻 LocalExpress1.112s (~)2.006s (~)0.895s101.02x
🌐 RedisNext.js (Turbopack)1.113s (+0.8%)2.013s (~)0.900s101.02x
💻 LocalNitro1.115s (~)2.007s (~)0.892s101.02x
🌐 MongoDBNext.js (Turbopack)1.261s (-4.0%)2.016s (~)0.755s101.16x
🌐 TursoNext.js (Turbopack)1.305s (~)2.013s (~)0.707s101.20x
🐘 PostgresExpress2.185s (+1.0%)3.015s (~)0.830s102.00x
🐘 PostgresNitro2.194s (+0.7%)3.015s (~)0.821s102.01x
🐘 PostgresNext.js (Turbopack)2.322s (+35.2% 🔺)2.818s (+33.2% 🔺)0.496s102.13x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)2.682s (-67.8% 🟢)3.592s (-59.7% 🟢)0.910s101.00x
▲ VercelExpress2.694s (-60.3% 🟢)3.718s (-51.6% 🟢)1.024s101.00x
▲ VercelNitro2.712s (-61.9% 🟢)3.698s (-53.0% 🟢)0.987s101.01x

🔍 Observability: Next.js (Turbopack) | Express | Nitro

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Starter🥇 Next.js (Turbopack)10.606s (~)11.012s (~)0.406s51.00x
💻 LocalNext.js (Turbopack)10.664s (~)11.014s (~)0.350s51.01x
🌐 RedisNext.js (Turbopack)10.708s (~)11.021s (~)0.314s51.01x
💻 LocalExpress10.787s (~)11.010s (~)0.223s51.02x
💻 LocalNitro10.789s (~)11.009s (~)0.220s51.02x
🌐 MongoDBNext.js (Turbopack)12.119s (-1.1%)12.829s (-1.5%)0.710s51.14x
🌐 TursoNext.js (Turbopack)12.175s (~)13.020s (~)0.845s51.15x
🐘 PostgresNext.js (Turbopack)15.410s (+2.7%)15.838s (+1.3%)0.428s51.45x
🐘 PostgresNitro20.351s (~)21.029s (~)0.678s51.92x
🐘 PostgresExpress20.416s (~)21.034s (~)0.618s51.92x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express21.836s (-1.2%)22.549s (-1.4%)0.713s51.00x
▲ VercelNext.js (Turbopack)22.144s (-0.7%)22.815s (~)0.671s51.01x
▲ VercelNitro22.426s (+2.0%)23.192s (+1.9%)0.767s51.03x

🔍 Observability: Express | Next.js (Turbopack) | Nitro

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Starter🥇 Next.js (Turbopack)1.355s (~)2.008s (~)0.653s151.00x
🌐 RedisNext.js (Turbopack)1.374s (+1.7%)2.011s (~)0.637s151.01x
💻 LocalNext.js (Turbopack)1.392s (~)2.013s (~)0.621s151.03x
💻 LocalNitro1.417s (+0.9%)2.005s (~)0.588s151.05x
💻 LocalExpress1.420s (+0.6%)2.007s (~)0.587s151.05x
🐘 PostgresNext.js (Turbopack)1.877s (+5.9% 🔺)2.014s (~)0.136s151.39x
🌐 MongoDBNext.js (Turbopack)2.144s (+1.0%)3.014s (~)0.870s101.58x
🌐 TursoNext.js (Turbopack)2.222s (+1.3%)3.012s (~)0.790s101.64x
🐘 PostgresExpress2.363s (~)3.012s (~)0.649s101.74x
🐘 PostgresNitro2.487s (+3.1%)3.012s (~)0.525s101.84x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.936s (-8.1% 🟢)3.789s (-5.6% 🟢)0.853s81.00x
▲ VercelExpress3.069s (+6.4% 🔺)3.925s (+7.8% 🔺)0.856s81.05x
▲ VercelNext.js (Turbopack)3.214s (+7.1% 🔺)3.982s (+6.0% 🔺)0.768s81.09x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)2.040s (-1.4%)2.896s (+3.9%)0.855s111.00x
💻 LocalExpress2.208s (~)3.181s (~)0.973s101.08x
💻 LocalNitro2.241s (+1.4%)3.168s (~)0.928s101.10x
🌐 StarterNext.js (Turbopack)2.483s (~)3.008s (~)0.525s101.22x
🌐 RedisNext.js (Turbopack)2.492s (+0.6%)3.012s (-0.7%)0.520s101.22x
🐘 PostgresNext.js (Turbopack)2.550s (-5.2% 🟢)3.020s (-0.8%)0.470s101.25x
🐘 PostgresExpress2.876s (-6.9% 🟢)3.232s (-17.3% 🟢)0.356s101.41x
🐘 PostgresNitro2.955s (~)3.114s (-7.0% 🟢)0.159s101.45x
🌐 MongoDBNext.js (Turbopack)4.650s (-0.6%)5.185s (~)0.535s62.28x
🌐 TursoNext.js (Turbopack)4.851s (+1.7%)5.180s (~)0.329s62.38x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.997s (+11.6% 🔺)4.820s (+16.3% 🔺)0.823s71.00x
▲ VercelExpress4.022s (+28.6% 🔺)4.751s (+23.7% 🔺)0.729s71.01x
▲ VercelNext.js (Turbopack)4.660s (+40.1% 🔺)5.415s (+38.1% 🔺)0.755s61.17x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Starter🥇 Next.js (Turbopack)1.359s (+0.5%)2.007s (~)0.648s151.00x
🌐 RedisNext.js (Turbopack)1.367s (~)2.011s (~)0.644s151.01x
💻 LocalExpress1.406s (-1.4%)2.006s (~)0.600s151.03x
💻 LocalNext.js (Turbopack)1.409s (+1.3%)2.015s (~)0.605s151.04x
💻 LocalNitro1.428s (~)2.005s (~)0.577s151.05x
🐘 PostgresNitro1.557s (-23.4% 🟢)2.010s (-16.1% 🟢)0.452s151.15x
🐘 PostgresNext.js (Turbopack)1.676s (-25.9% 🟢)2.013s (-25.2% 🟢)0.337s151.23x
🐘 PostgresExpress1.836s (+3.1%)2.010s (-3.3%)0.174s151.35x
🌐 MongoDBNext.js (Turbopack)2.116s (-0.9%)3.019s (~)0.903s101.56x
🌐 TursoNext.js (Turbopack)2.219s (-1.2%)3.013s (~)0.794s101.63x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)2.837s (+2.4%)3.571s (-2.7%)0.735s91.00x
▲ VercelExpress3.139s (+7.3% 🔺)4.119s (+10.8% 🔺)0.980s81.11x
▲ VercelNitro3.218s (-5.2% 🟢)4.497s (+2.9%)1.279s71.13x

🔍 Observability: Next.js (Turbopack) | Express | Nitro

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express2.235s (+0.7%)3.197s (+0.6%)0.963s101.00x
💻 LocalNext.js (Turbopack)2.307s (+8.1% 🔺)3.142s (+3.7%)0.835s101.03x
💻 LocalNitro2.378s (+3.5%)3.317s (+2.9%)0.939s101.06x
🌐 StarterNext.js (Turbopack)2.459s (~)3.008s (~)0.549s101.10x
🐘 PostgresNitro2.497s (-18.3% 🟢)3.017s (-15.7% 🟢)0.520s101.12x
🌐 RedisNext.js (Turbopack)2.517s (+1.3%)3.012s (~)0.495s101.13x
🐘 PostgresNext.js (Turbopack)2.583s (-5.0%)3.029s (-0.8%)0.447s101.16x
🐘 PostgresExpress2.642s (-8.9% 🟢)3.018s (-6.5% 🟢)0.377s101.18x
🌐 MongoDBNext.js (Turbopack)4.711s (~)5.181s (~)0.470s62.11x
🌐 TursoNext.js (Turbopack)4.778s (+1.6%)5.183s (-3.1%)0.405s62.14x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.209s (+2.5%)3.778s (+0.8%)0.569s81.00x
▲ VercelExpress3.254s (~)3.848s (+2.4%)0.594s81.01x
▲ VercelNext.js (Turbopack)3.454s (+0.9%)4.032s (~)0.578s81.08x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🌐 Starter🥇 Next.js (Turbopack)0.129s (~)1.005s (~)0.000s (NaN%)1.011s (~)0.882s101.00x
💻 LocalNext.js (Turbopack)0.141s (-3.4%)1.003s (~)0.017s (-0.6%)1.028s (~)0.887s101.09x
🌐 RedisNext.js (Turbopack)0.159s (+11.6% 🔺)1.005s (~)0.000s (-100.0% 🟢)1.015s (~)0.855s101.23x
💻 LocalExpress0.176s (-0.7%)0.992s (~)0.015s (-1.9%)1.022s (~)0.846s101.36x
💻 LocalNitro0.182s (+1.7%)0.992s (~)0.015s (-0.7%)1.021s (~)0.838s101.41x
🌐 TursoNext.js (Turbopack)0.488s (+6.3% 🔺)0.965s (-3.0%)0.000s (+Infinity% 🔺)1.013s (~)0.524s103.78x
🌐 MongoDBNext.js (Turbopack)0.506s (-3.3%)0.941s (+1.7%)0.000s (+Infinity% 🔺)1.015s (~)0.509s103.92x
🐘 PostgresNext.js (Turbopack)0.673s (-29.9% 🟢)1.052s (-1.5%)0.000s (-100.0% 🟢)1.118s (-15.1% 🟢)0.444s105.21x
🐘 PostgresExpress2.313s (+3.0%)2.728s (-2.7%)0.000s (-100.0% 🟢)3.014s (~)0.700s1017.90x
🐘 PostgresNitro2.402s (+3.2%)2.639s (-2.9%)0.000s (+Infinity% 🔺)3.014s (~)0.611s1018.59x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)2.826s (~)3.323s (+6.2% 🔺)0.378s (-49.5% 🟢)4.114s (-4.7%)1.288s101.00x
▲ VercelNitro2.891s (-0.7%)3.301s (+3.4%)0.454s (-23.0% 🟢)4.226s (-0.6%)1.335s101.02x
▲ VercelExpress2.914s (+1.5%)3.295s (+3.9%)0.396s (-58.0% 🟢)4.190s (-8.4% 🟢)1.276s101.03x

🔍 Observability: Next.js (Turbopack) | Nitro | Express

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalNext.js (Turbopack)6/8
🐘 PostgresNext.js (Turbopack)5/8
▲ VercelNitro4/8
Fastest World by Framework

Winner determined by most benchmark wins

Framework🥇 Fastest WorldWins
Express💻 Local8/8
Next.js (Turbopack)🌐 Starter6/8
Nitro💻 Local8/8
Column Definitions
  • Workflow Time: Runtime reported by workflow (completedAt - createdAt) - primary metric
  • TTFB: Time to First Byte - time from workflow start until first stream byte received (stream benchmarks only)
  • Slurp: Time from first byte to complete stream consumption (stream benchmarks only)
  • Wall Time: Total testbench time (trigger workflow + poll for result)
  • Overhead: Testbench overhead (Wall Time - Workflow Time)
  • Samples: Number of benchmark iterations run
  • vs Fastest: How much slower compared to the fastest configuration for this benchmark

Worlds:

  • 💻 Local: In-memory filesystem world (local development)
  • 🐘 Postgres: PostgreSQL database world (local development)
  • ▲ Vercel: Vercel production/preview deployment
  • 🌐 Starter: Community world (local development)
  • 🌐 Turso: Community world (local development)
  • 🌐 MongoDB: Community world (local development)
  • 🌐 Redis: Community world (local development)
  • 🌐 Jazz: Community world (local development)

📋 View full workflow run

@VaguelySeriousVaguelySerious left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM ✨

One styling change:

className="inline-flex flex-col rounded text-[11px] font-mono my-1"
style={{
backgroundColor: colors.body,
border: `1px solid ${colors.header}`,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This leaves some weird extra white pixel borders

Suggested change
border: `1px solid ${colors.header}`,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually I tested too early, this seems to already be resolved

@socket-security

socket-securityBot commented Jan 20, 2026

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

DiffPackageSupply Chain
Security
VulnerabilityQualityMaintenanceLicense
Addednpm/​@​types/​color-hash@​2.0.01001009080100
Addednpm/​color-hash@​2.0.210010010080100

View full report

Comment thread.changeset/forty-tables-lick.md Outdated
TooTallNateand others added 2 commits January 20, 2026 01:39
Co-authored-by: Peter Wielander <mittgfu@gmail.com>
Signed-off-by: Nathan Rajlich <n@n8.io>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@TooTallNate@VaguelySerious
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Show opaque marker when deserializing custom classes and instances in o11y by TooTallNate · Pull Request #809 · vercel/workflow · GitHub
Skip to content

Show opaque marker when deserializing custom classes and instances in o11y - #809

Merged
TooTallNate merged 8 commits into
mainfrom
01-19-show_opaque_marker_when_deserializing_custom_classes_and_instances_in_o11y
Jan 20, 2026
Merged

Show opaque marker when deserializing custom classes and instances in o11y#809
TooTallNate merged 8 commits into
mainfrom
01-19-show_opaque_marker_when_deserializing_custom_classes_and_instances_in_o11y

Conversation

@TooTallNate

@TooTallNateTooTallNate commented Jan 19, 2026

Copy link
Copy Markdown
Member

Web example:

dark mode
Screenshot 2026-01-20 at 01 38 04

light mode
Screenshot 2026-01-20 at 01 38 09

CLI example:

Screenshot 2026-01-20 at 01 44 53

Added support for displaying custom class instances in the observability UI by showing opaque markers when deserializing custom classes.

What changed?

  • Added a CLASS_INSTANCE_REF_TYPE marker and related interfaces to represent serialized class instances that cannot be fully deserialized
  • Implemented helper functions to extract class names from class IDs and convert serialized instances to reference objects
  • Enhanced the stream print revivers to handle custom class instances and class references
  • Updated the attribute panel to recognize and display class instance references with a __class__ marker alongside the serialized data

How to test?

  1. Create a workflow that uses custom classes
  2. Run the workflow and view it in the observability UI
  3. Verify that custom class instances are displayed with their class names and serialized data
  4. Check that class references are shown as <class:ClassName> in the UI

Why make this change?

Previously, custom class instances couldn't be properly displayed in the observability UI because they weren't registered for deserialization in the o11y context. This change improves the developer experience by providing meaningful representations of custom classes and instances, making it easier to debug and understand workflow execution without requiring class registration.

@changeset-bot

changeset-botBot commented Jan 19, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: c889dd1

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 13 packages
NameType
@workflow/web-sharedPatch
@workflow/corePatch
@workflow/buildersPatch
@workflow/cliPatch
@workflow/docs-typecheckPatch
@workflow/nextPatch
@workflow/nitroPatch
workflowPatch
@workflow/astroPatch
@workflow/sveltekitPatch
@workflow/world-testingPatch
@workflow/nuxtPatch
@workflow/aiPatch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions

github-actionsBot commented Jan 19, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production435038473
❌ 💻 Local Development396232430
✅ 📦 Local Production398032430
✅ 🐘 Local Postgres398032430
✅ 🪟 Windows430043
❌ 🌍 Community Worlds162220184
Total1832241341990

❌ Failed Tests

💻 Local Development (2 failed)

sveltekit-stable (2 failed):

  • error handling error propagation step errors basic step error preserves message and stack trace
  • error handling error propagation step errors cross-file step error preserves message and function names in stack
🌍 Community Worlds (22 failed)

mongodb (1 failed):

  • webhookWorkflow

redis (1 failed):

  • webhookWorkflow

starter (19 failed):

  • addTenWorkflow
  • addTenWorkflow
  • error handling error propagation workflow errors nested function calls preserve message and stack trace
  • error handling error propagation workflow errors cross-file imports preserve message and stack trace
  • error handling error propagation step errors basic step error preserves message and stack trace
  • error handling error propagation step errors cross-file step error preserves message and function names in stack
  • error handling retry behavior regular Error retries until success
  • error handling retry behavior FatalError fails immediately without retries
  • error handling catchability FatalError can be caught and detected with FatalError.is()
  • hookCleanupTestWorkflow - hook token reuse after workflow completion
  • stepFunctionPassingWorkflow - step function references can be passed as arguments (without closure vars)
  • stepFunctionWithClosureWorkflow - step function with closure variables passed as argument
  • spawnWorkflowFromStepWorkflow - spawning a child workflow using start() inside a step
  • pathsAliasWorkflow - TypeScript path aliases resolve correctly
  • Calculator.calculate - static workflow method using static step methods from another class
  • AllInOneService.processNumber - static workflow method using sibling static step methods
  • ChainableService.processWithThis - static step methods using this to reference the class
  • thisSerializationWorkflow - step function invoked with .call() and .apply()
  • customSerializationWorkflow - custom class serialization with WORKFLOW_SERIALIZE/WORKFLOW_DESERIALIZE

turso (1 failed):

  • webhookWorkflow

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro3904
✅ example3904
✅ express3904
✅ fastify3904
✅ hono3904
✅ nextjs-turbopack4201
✅ nextjs-webpack4201
✅ nitro3904
✅ nuxt3904
✅ sveltekit3904
✅ vite3904
❌ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable3904
✅ express-stable3904
✅ fastify-stable3904
✅ hono-stable3904
✅ nextjs-turbopack-stable4300
✅ nextjs-webpack-stable4300
✅ nitro-stable3904
✅ nuxt-stable3904
❌ sveltekit-stable3724
✅ vite-stable3904
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable3904
✅ express-stable3904
✅ fastify-stable3904
✅ hono-stable3904
✅ nextjs-turbopack-stable4300
✅ nextjs-webpack-stable4300
✅ nitro-stable3904
✅ nuxt-stable3904
✅ sveltekit-stable3904
✅ vite-stable3904
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable3904
✅ express-stable3904
✅ fastify-stable3904
✅ hono-stable3904
✅ nextjs-turbopack-stable4300
✅ nextjs-webpack-stable4300
✅ nitro-stable3904
✅ nuxt-stable3904
✅ sveltekit-stable3904
✅ vite-stable3904
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack4300
❌ 🌍 Community Worlds
AppPassedFailedSkipped
✅ mongodb-dev300
❌ mongodb4210
✅ redis-dev300
❌ redis4210
✅ starter-dev300
❌ starter24190
✅ turso-dev300
❌ turso4210

📋 View full workflow run


Some E2E test jobs failed:

  • Vercel Prod: success
  • Local Dev: failure
  • Local Prod: success
  • Local Postgres: success
  • Windows: success

Check the workflow run for details.

@TooTallNateGraphite App

TooTallNate commented Jan 19, 2026

Copy link
Copy Markdown
MemberAuthor

@TooTallNate
TooTallNate marked this pull request as ready for review January 19, 2026 22:17
CopilotAI review requested due to automatic review settings January 19, 2026 22:17
@vercel

vercelBot commented Jan 19, 2026

Copy link
Copy Markdown
Contributor

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR enhances the observability UI to display custom class instances that cannot be fully deserialized because their classes are not registered in the o11y context. The implementation adds opaque markers for both class instances and class references, making it easier to debug workflows that use custom classes.

Changes:

  • Added CLASS_INSTANCE_REF_TYPE marker, ClassInstanceRef interface, and type guards to represent unregistered class instances
  • Implemented helper functions (extractClassName, serializedInstanceToRef, serializedClassToString) to convert serialized class data to display-friendly formats
  • Extended streamPrintRevivers to handle Instance and Class types by converting them to opaque markers
  • Updated UI transformation logic to display class instances inline with a __class__ marker alongside their serialized data

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 8 comments.

FileDescription
packages/core/src/observability.tsAdds ClassInstanceRef types, helper functions for class name extraction, and stream print revivers for Instance and Class types
packages/web-shared/src/sidebar/attribute-panel.tsxDuplicates ClassInstanceRef types for client-side use and updates transformValueForDisplay to handle class instance refs inline with class marker
.changeset/forty-tables-lick.mdDocuments the patch-level changes for both packages

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

'__type' in value &&
value.__type === CLASS_INSTANCE_REF_TYPE &&
'className' in value &&
typeof value.className === 'string'

CopilotAIJan 19, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The type guard isClassInstanceRef validates className but not classId. For consistency with isStreamRef which validates all required fields, and to ensure type safety, the function should also validate that classId exists and is a string.

Suggested change
typeofvalue.className==='string'
typeofvalue.className==='string'&&
'classId'invalue&&
typeofvalue.classId==='string'

Copilot uses AI. Check for mistakes.
Comment on lines +124 to +128
const extractClassName = (classId: string): string => {
if (!classId) return 'Unknown';
const parts = classId.split('/');
return parts[parts.length - 1] || classId;
};

CopilotAIJan 19, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The extractClassName function returns 'Unknown' when classId is falsy, but an empty string is falsy in JavaScript. When classId is an empty string, the function would return 'Unknown' without trying to split it. However, if the split produces an empty array or the last part is an empty string, it would fall back to returning the original classId. Consider handling the empty string case explicitly before the split operation to ensure consistent behavior.

Copilot uses AI. Check for mistakes.
Comment on lines +244 to +249
return {
__class__: v.className,
...(typeof v.data === 'object' && v.data !== null
? v.data
: { value: v.data }),
};

CopilotAIJan 19, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The spread operator on line 247 could potentially overwrite the class property if v.data is an object that contains a class key. This would result in the class name information being lost. Consider using a different marker name that's less likely to collide with actual object properties, or ensure the class property cannot be overwritten by spreading v.data after it instead of before.

Suggested change
return{
__class__: v.className,
...(typeofv.data==='object'&&v.data!==null
? v.data
: {value: v.data}),
};
returntypeofv.data==='object'&&v.data!==null
? {
...(v.dataasRecord<string,unknown>),
__class__: v.className,
}
: {
value: v.data,
__class__: v.className,
};

Copilot uses AI. Check for mistakes.
Comment on lines +244 to +248
return {
__class__: v.className,
...(typeof v.data === 'object' && v.data !== null
? v.data
: { value: v.data }),

CopilotAIJan 19, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The v.data object is spread directly without being transformed recursively. If v.data contains nested StreamRef or ClassInstanceRef objects, they won't be properly handled. The nested data should be recursively transformed before spreading to ensure all special references are properly converted.

Suggested change
return{
__class__: v.className,
...(typeofv.data==='object'&&v.data!==null
? v.data
: {value: v.data}),
consttransformedData=
typeofv.data==='object'&&v.data!==null
? transform(v.data)
: transform(v.data);
return{
__class__: v.className,
...(typeofv.data==='object'&&v.data!==null
? (transformedDataasRecord<string,unknown>)
: {value: transformedData}),

Copilot uses AI. Check for mistakes.
Comment on lines +151 to 157
const serializedClassToString = (value: { classId: string }): string => {
const className = extractClassName(value.classId);
return `<class:${className}>`;
};

/**
* This is an extra reviver for devalue that takes any streams that would be converted,

CopilotAIJan 19, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The serializedClassToString function doesn't validate that the input value has the expected structure. If value.classId is undefined or not a string, extractClassName will handle it, but it would be clearer to add explicit validation to ensure the function is called with the expected shape of data.

Suggested change
constserializedClassToString=(value: {classId: string}): string=>{
constclassName=extractClassName(value.classId);
return`<class:${className}>`;
};
/**
*Thisisanextrareviverfordevaluethattakesanystreamsthatwouldbeconverted,
*Convertaserializedclassreferencetoa string representation.
*ThisisusedforClasstype(theconstructorreferenceitself,notaninstance).
*/
constserializedClassToString=(value: unknown): string=>{
if(
!value||
typeofvalue!=='object'||
!('classId'invalue)||
typeof(valueas{classId: unknown}).classId!=='string'
){
return'<class:Unknown>';
}
constclassId=(valueas{classId: string}).classId;
constclassName=extractClassName(classId);
return`<class:${className}>`;
};

Copilot uses AI. Check for mistakes.
Comment threadpackages/core/src/observability.ts Outdated
Comment on lines +139 to +143
return {
__type: CLASS_INSTANCE_REF_TYPE,
className: extractClassName(value.classId),
classId: value.classId,
data: value.data,

CopilotAIJan 19, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The serializedInstanceToRef function doesn't validate that the input value has the expected structure. While the type annotation suggests the expected shape, adding runtime validation would make the function more robust, especially since it's called from the streamPrintRevivers which receives values of type any.

Suggested change
return{
__type: CLASS_INSTANCE_REF_TYPE,
className: extractClassName(value.classId),
classId: value.classId,
data: value.data,
// Runtime validation because this function is ultimately called with values of type `any`.
constisObject=value!==null&&typeofvalue==='object';
constclassId=isObject&&typeof(valueasany).classId==='string'
? (valueasany).classId
: '';
constdata=isObject ? (valueasany).data : value;
return{
__type: CLASS_INSTANCE_REF_TYPE,
className: extractClassName(classId),
classId,
data,

Copilot uses AI. Check for mistakes.
Comment on lines +120 to +154
/**
* Extract the class name from a classId.
* The classId format is typically "path/to/file/ClassName" so we extract the last segment.
*/
const extractClassName = (classId: string): string => {
if (!classId) return 'Unknown';
const parts = classId.split('/');
return parts[parts.length - 1] || classId;
};

/**
* Convert a serialized class instance to a ClassInstanceRef for o11y display.
* This allows viewing custom class instances in the UI without needing
* the class to be registered for deserialization.
*/
const serializedInstanceToRef = (value: {
classId: string;
data: unknown;
}): ClassInstanceRef => {
return {
__type: CLASS_INSTANCE_REF_TYPE,
className: extractClassName(value.classId),
classId: value.classId,
data: value.data,
};
};

/**
* Convert a serialized class reference to a string representation.
* This is used for Class type (the constructor reference itself, not an instance).
*/
const serializedClassToString = (value: { classId: string }): string => {
const className = extractClassName(value.classId);
return `<class:${className}>`;
};

CopilotAIJan 19, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new helper functions extractClassName, serializedInstanceToRef, and serializedClassToString lack test coverage. Given that other utility functions in the codebase have comprehensive test files (e.g., serialization.test.ts, parse-name.test.ts), consider adding tests to verify the behavior of these functions, especially edge cases like empty classId, classIds with different path separators, and handling of various data types.

Copilot uses AI. Check for mistakes.
Comment threadpackages/core/src/observability.ts Outdated
'__type' in value &&
value.__type === CLASS_INSTANCE_REF_TYPE &&
'className' in value &&
typeof value.className === 'string'

CopilotAIJan 19, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The type guard isClassInstanceRef validates className but not classId. For consistency with isStreamRef which validates all required fields, and to ensure type safety, the function should also validate that classId exists and is a string. This would prevent runtime errors if a malformed ClassInstanceRef object is encountered.

Suggested change
typeofvalue.className==='string'
typeofvalue.className==='string'&&
'classId'invalue&&
typeofvalue.classId==='string'

Copilot uses AI. Check for mistakes.
@TooTallNate
TooTallNateforce-pushed the 01-19-show_opaque_marker_when_deserializing_custom_classes_and_instances_in_o11y branch from 127cfec to c46327fCompareJanuary 19, 2026 22:26
@TooTallNate
TooTallNateforce-pushed the 01-09-add_support_for_custom_class_instance_serialization branch from baea85a to 4216347CompareJanuary 19, 2026 22:26
value: unknown
): { json: string; streamRefs: Map<string, StreamRef> } => {
): {
json: string;

@vercelvercelBotJan 19, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ClassInstanceRef transformation allows properties in v.data to overwrite the class marker, and doesn't recursively transform nested ClassInstanceRef objects within v.data

Fix on Vercel

* Used in o11y when a custom class instance is encountered but the class is not
* registered for deserialization.
*/
interface ClassInstanceRef {

@vercelvercelBotJan 19, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Type guard isClassInstanceRef doesn't validate classId and data properties required by ClassInstanceRef interface, leading to type unsafety

Fix on Vercel

Comment threadpackages/core/src/observability.ts Outdated
'__type' in value &&
value.__type === CLASS_INSTANCE_REF_TYPE &&
'className' in value &&
typeof value.className === 'string'

@vercelvercelBotJan 19, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The isClassInstanceRef type guard doesn't validate classId and data properties required by the ClassInstanceRef interface

Fix on Vercel

@TooTallNate
TooTallNateforce-pushed the 01-19-show_opaque_marker_when_deserializing_custom_classes_and_instances_in_o11y branch from a14fe6d to 04b36f5CompareJanuary 19, 2026 23:14
@github-actions

github-actionsBot commented Jan 19, 2026

Copy link
Copy Markdown
Contributor

📊 Benchmark Results

📈 Comparing against baseline from main branch. Green 🟢 = faster, Red 🔺 = slower.

workflow with no steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Starter🥇 Next.js (Turbopack)0.040s (-1.2%)1.014s (~)0.974s101.00x
💻 LocalNext.js (Turbopack)0.042s (+2.7%)1.019s (~)0.977s101.06x
💻 LocalNitro0.043s (-3.3%)1.007s (~)0.964s101.09x
💻 LocalExpress0.044s (-1.1%)1.007s (~)0.963s101.11x
🌐 RedisNext.js (Turbopack)0.045s (+11.9% 🔺)1.018s (~)0.973s101.14x
🌐 TursoNext.js (Turbopack)0.106s (-3.1%)1.013s (~)0.908s102.66x
🌐 MongoDBNext.js (Turbopack)0.122s (+123.6% 🔺)1.018s (~)0.895s103.08x
🐘 PostgresNext.js (Turbopack)0.176s (-44.7% 🟢)1.022s (~)0.845s104.44x
🐘 PostgresNitro0.337s (+6.8% 🔺)1.012s (~)0.675s108.49x
🐘 PostgresExpress0.338s (+5.0% 🔺)1.013s (~)0.675s108.52x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro0.575s (-80.7% 🟢)1.580s (-58.0% 🟢)1.005s101.00x
▲ VercelExpress0.689s (-83.2% 🟢)1.686s (-66.5% 🟢)0.997s101.20x
▲ VercelNext.js (Turbopack)0.744s (-66.1% 🟢)1.568s (-45.5% 🟢)0.824s101.29x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Starter🥇 Next.js (Turbopack)1.090s (~)2.010s (~)0.920s101.00x
💻 LocalNext.js (Turbopack)1.098s (+0.6%)2.012s (~)0.914s101.01x
💻 LocalExpress1.112s (~)2.006s (~)0.895s101.02x
🌐 RedisNext.js (Turbopack)1.113s (+0.8%)2.013s (~)0.900s101.02x
💻 LocalNitro1.115s (~)2.007s (~)0.892s101.02x
🌐 MongoDBNext.js (Turbopack)1.261s (-4.0%)2.016s (~)0.755s101.16x
🌐 TursoNext.js (Turbopack)1.305s (~)2.013s (~)0.707s101.20x
🐘 PostgresExpress2.185s (+1.0%)3.015s (~)0.830s102.00x
🐘 PostgresNitro2.194s (+0.7%)3.015s (~)0.821s102.01x
🐘 PostgresNext.js (Turbopack)2.322s (+35.2% 🔺)2.818s (+33.2% 🔺)0.496s102.13x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)2.682s (-67.8% 🟢)3.592s (-59.7% 🟢)0.910s101.00x
▲ VercelExpress2.694s (-60.3% 🟢)3.718s (-51.6% 🟢)1.024s101.00x
▲ VercelNitro2.712s (-61.9% 🟢)3.698s (-53.0% 🟢)0.987s101.01x

🔍 Observability: Next.js (Turbopack) | Express | Nitro

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Starter🥇 Next.js (Turbopack)10.606s (~)11.012s (~)0.406s51.00x
💻 LocalNext.js (Turbopack)10.664s (~)11.014s (~)0.350s51.01x
🌐 RedisNext.js (Turbopack)10.708s (~)11.021s (~)0.314s51.01x
💻 LocalExpress10.787s (~)11.010s (~)0.223s51.02x
💻 LocalNitro10.789s (~)11.009s (~)0.220s51.02x
🌐 MongoDBNext.js (Turbopack)12.119s (-1.1%)12.829s (-1.5%)0.710s51.14x
🌐 TursoNext.js (Turbopack)12.175s (~)13.020s (~)0.845s51.15x
🐘 PostgresNext.js (Turbopack)15.410s (+2.7%)15.838s (+1.3%)0.428s51.45x
🐘 PostgresNitro20.351s (~)21.029s (~)0.678s51.92x
🐘 PostgresExpress20.416s (~)21.034s (~)0.618s51.92x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express21.836s (-1.2%)22.549s (-1.4%)0.713s51.00x
▲ VercelNext.js (Turbopack)22.144s (-0.7%)22.815s (~)0.671s51.01x
▲ VercelNitro22.426s (+2.0%)23.192s (+1.9%)0.767s51.03x

🔍 Observability: Express | Next.js (Turbopack) | Nitro

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Starter🥇 Next.js (Turbopack)1.355s (~)2.008s (~)0.653s151.00x
🌐 RedisNext.js (Turbopack)1.374s (+1.7%)2.011s (~)0.637s151.01x
💻 LocalNext.js (Turbopack)1.392s (~)2.013s (~)0.621s151.03x
💻 LocalNitro1.417s (+0.9%)2.005s (~)0.588s151.05x
💻 LocalExpress1.420s (+0.6%)2.007s (~)0.587s151.05x
🐘 PostgresNext.js (Turbopack)1.877s (+5.9% 🔺)2.014s (~)0.136s151.39x
🌐 MongoDBNext.js (Turbopack)2.144s (+1.0%)3.014s (~)0.870s101.58x
🌐 TursoNext.js (Turbopack)2.222s (+1.3%)3.012s (~)0.790s101.64x
🐘 PostgresExpress2.363s (~)3.012s (~)0.649s101.74x
🐘 PostgresNitro2.487s (+3.1%)3.012s (~)0.525s101.84x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.936s (-8.1% 🟢)3.789s (-5.6% 🟢)0.853s81.00x
▲ VercelExpress3.069s (+6.4% 🔺)3.925s (+7.8% 🔺)0.856s81.05x
▲ VercelNext.js (Turbopack)3.214s (+7.1% 🔺)3.982s (+6.0% 🔺)0.768s81.09x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)2.040s (-1.4%)2.896s (+3.9%)0.855s111.00x
💻 LocalExpress2.208s (~)3.181s (~)0.973s101.08x
💻 LocalNitro2.241s (+1.4%)3.168s (~)0.928s101.10x
🌐 StarterNext.js (Turbopack)2.483s (~)3.008s (~)0.525s101.22x
🌐 RedisNext.js (Turbopack)2.492s (+0.6%)3.012s (-0.7%)0.520s101.22x
🐘 PostgresNext.js (Turbopack)2.550s (-5.2% 🟢)3.020s (-0.8%)0.470s101.25x
🐘 PostgresExpress2.876s (-6.9% 🟢)3.232s (-17.3% 🟢)0.356s101.41x
🐘 PostgresNitro2.955s (~)3.114s (-7.0% 🟢)0.159s101.45x
🌐 MongoDBNext.js (Turbopack)4.650s (-0.6%)5.185s (~)0.535s62.28x
🌐 TursoNext.js (Turbopack)4.851s (+1.7%)5.180s (~)0.329s62.38x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.997s (+11.6% 🔺)4.820s (+16.3% 🔺)0.823s71.00x
▲ VercelExpress4.022s (+28.6% 🔺)4.751s (+23.7% 🔺)0.729s71.01x
▲ VercelNext.js (Turbopack)4.660s (+40.1% 🔺)5.415s (+38.1% 🔺)0.755s61.17x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Starter🥇 Next.js (Turbopack)1.359s (+0.5%)2.007s (~)0.648s151.00x
🌐 RedisNext.js (Turbopack)1.367s (~)2.011s (~)0.644s151.01x
💻 LocalExpress1.406s (-1.4%)2.006s (~)0.600s151.03x
💻 LocalNext.js (Turbopack)1.409s (+1.3%)2.015s (~)0.605s151.04x
💻 LocalNitro1.428s (~)2.005s (~)0.577s151.05x
🐘 PostgresNitro1.557s (-23.4% 🟢)2.010s (-16.1% 🟢)0.452s151.15x
🐘 PostgresNext.js (Turbopack)1.676s (-25.9% 🟢)2.013s (-25.2% 🟢)0.337s151.23x
🐘 PostgresExpress1.836s (+3.1%)2.010s (-3.3%)0.174s151.35x
🌐 MongoDBNext.js (Turbopack)2.116s (-0.9%)3.019s (~)0.903s101.56x
🌐 TursoNext.js (Turbopack)2.219s (-1.2%)3.013s (~)0.794s101.63x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)2.837s (+2.4%)3.571s (-2.7%)0.735s91.00x
▲ VercelExpress3.139s (+7.3% 🔺)4.119s (+10.8% 🔺)0.980s81.11x
▲ VercelNitro3.218s (-5.2% 🟢)4.497s (+2.9%)1.279s71.13x

🔍 Observability: Next.js (Turbopack) | Express | Nitro

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express2.235s (+0.7%)3.197s (+0.6%)0.963s101.00x
💻 LocalNext.js (Turbopack)2.307s (+8.1% 🔺)3.142s (+3.7%)0.835s101.03x
💻 LocalNitro2.378s (+3.5%)3.317s (+2.9%)0.939s101.06x
🌐 StarterNext.js (Turbopack)2.459s (~)3.008s (~)0.549s101.10x
🐘 PostgresNitro2.497s (-18.3% 🟢)3.017s (-15.7% 🟢)0.520s101.12x
🌐 RedisNext.js (Turbopack)2.517s (+1.3%)3.012s (~)0.495s101.13x
🐘 PostgresNext.js (Turbopack)2.583s (-5.0%)3.029s (-0.8%)0.447s101.16x
🐘 PostgresExpress2.642s (-8.9% 🟢)3.018s (-6.5% 🟢)0.377s101.18x
🌐 MongoDBNext.js (Turbopack)4.711s (~)5.181s (~)0.470s62.11x
🌐 TursoNext.js (Turbopack)4.778s (+1.6%)5.183s (-3.1%)0.405s62.14x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.209s (+2.5%)3.778s (+0.8%)0.569s81.00x
▲ VercelExpress3.254s (~)3.848s (+2.4%)0.594s81.01x
▲ VercelNext.js (Turbopack)3.454s (+0.9%)4.032s (~)0.578s81.08x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🌐 Starter🥇 Next.js (Turbopack)0.129s (~)1.005s (~)0.000s (NaN%)1.011s (~)0.882s101.00x
💻 LocalNext.js (Turbopack)0.141s (-3.4%)1.003s (~)0.017s (-0.6%)1.028s (~)0.887s101.09x
🌐 RedisNext.js (Turbopack)0.159s (+11.6% 🔺)1.005s (~)0.000s (-100.0% 🟢)1.015s (~)0.855s101.23x
💻 LocalExpress0.176s (-0.7%)0.992s (~)0.015s (-1.9%)1.022s (~)0.846s101.36x
💻 LocalNitro0.182s (+1.7%)0.992s (~)0.015s (-0.7%)1.021s (~)0.838s101.41x
🌐 TursoNext.js (Turbopack)0.488s (+6.3% 🔺)0.965s (-3.0%)0.000s (+Infinity% 🔺)1.013s (~)0.524s103.78x
🌐 MongoDBNext.js (Turbopack)0.506s (-3.3%)0.941s (+1.7%)0.000s (+Infinity% 🔺)1.015s (~)0.509s103.92x
🐘 PostgresNext.js (Turbopack)0.673s (-29.9% 🟢)1.052s (-1.5%)0.000s (-100.0% 🟢)1.118s (-15.1% 🟢)0.444s105.21x
🐘 PostgresExpress2.313s (+3.0%)2.728s (-2.7%)0.000s (-100.0% 🟢)3.014s (~)0.700s1017.90x
🐘 PostgresNitro2.402s (+3.2%)2.639s (-2.9%)0.000s (+Infinity% 🔺)3.014s (~)0.611s1018.59x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)2.826s (~)3.323s (+6.2% 🔺)0.378s (-49.5% 🟢)4.114s (-4.7%)1.288s101.00x
▲ VercelNitro2.891s (-0.7%)3.301s (+3.4%)0.454s (-23.0% 🟢)4.226s (-0.6%)1.335s101.02x
▲ VercelExpress2.914s (+1.5%)3.295s (+3.9%)0.396s (-58.0% 🟢)4.190s (-8.4% 🟢)1.276s101.03x

🔍 Observability: Next.js (Turbopack) | Nitro | Express

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalNext.js (Turbopack)6/8
🐘 PostgresNext.js (Turbopack)5/8
▲ VercelNitro4/8
Fastest World by Framework

Winner determined by most benchmark wins

Framework🥇 Fastest WorldWins
Express💻 Local8/8
Next.js (Turbopack)🌐 Starter6/8
Nitro💻 Local8/8
Column Definitions
  • Workflow Time: Runtime reported by workflow (completedAt - createdAt) - primary metric
  • TTFB: Time to First Byte - time from workflow start until first stream byte received (stream benchmarks only)
  • Slurp: Time from first byte to complete stream consumption (stream benchmarks only)
  • Wall Time: Total testbench time (trigger workflow + poll for result)
  • Overhead: Testbench overhead (Wall Time - Workflow Time)
  • Samples: Number of benchmark iterations run
  • vs Fastest: How much slower compared to the fastest configuration for this benchmark

Worlds:

  • 💻 Local: In-memory filesystem world (local development)
  • 🐘 Postgres: PostgreSQL database world (local development)
  • ▲ Vercel: Vercel production/preview deployment
  • 🌐 Starter: Community world (local development)
  • 🌐 Turso: Community world (local development)
  • 🌐 MongoDB: Community world (local development)
  • 🌐 Redis: Community world (local development)
  • 🌐 Jazz: Community world (local development)

📋 View full workflow run

@VaguelySeriousVaguelySerious left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM ✨

One styling change:

className="inline-flex flex-col rounded text-[11px] font-mono my-1"
style={{
backgroundColor: colors.body,
border: `1px solid ${colors.header}`,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This leaves some weird extra white pixel borders

Suggested change
border: `1px solid ${colors.header}`,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually I tested too early, this seems to already be resolved

@socket-security

socket-securityBot commented Jan 20, 2026

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

DiffPackageSupply Chain
Security
VulnerabilityQualityMaintenanceLicense
Addednpm/​@​types/​color-hash@​2.0.01001009080100
Addednpm/​color-hash@​2.0.210010010080100

View full report

Comment thread.changeset/forty-tables-lick.md Outdated
TooTallNateand others added 2 commits January 20, 2026 01:39
Co-authored-by: Peter Wielander <mittgfu@gmail.com>
Signed-off-by: Nathan Rajlich <n@n8.io>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@TooTallNate@VaguelySerious
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Show opaque marker when deserializing custom classes and instances in o11y by TooTallNate · Pull Request #809 · vercel/workflow · GitHub
Skip to content

Show opaque marker when deserializing custom classes and instances in o11y - #809

Merged
TooTallNate merged 8 commits into
mainfrom
01-19-show_opaque_marker_when_deserializing_custom_classes_and_instances_in_o11y
Jan 20, 2026
Merged

Show opaque marker when deserializing custom classes and instances in o11y#809
TooTallNate merged 8 commits into
mainfrom
01-19-show_opaque_marker_when_deserializing_custom_classes_and_instances_in_o11y

Conversation

@TooTallNate

@TooTallNateTooTallNate commented Jan 19, 2026

Copy link
Copy Markdown
Member

Web example:

dark mode
Screenshot 2026-01-20 at 01 38 04

light mode
Screenshot 2026-01-20 at 01 38 09

CLI example:

Screenshot 2026-01-20 at 01 44 53

Added support for displaying custom class instances in the observability UI by showing opaque markers when deserializing custom classes.

What changed?

  • Added a CLASS_INSTANCE_REF_TYPE marker and related interfaces to represent serialized class instances that cannot be fully deserialized
  • Implemented helper functions to extract class names from class IDs and convert serialized instances to reference objects
  • Enhanced the stream print revivers to handle custom class instances and class references
  • Updated the attribute panel to recognize and display class instance references with a __class__ marker alongside the serialized data

How to test?

  1. Create a workflow that uses custom classes
  2. Run the workflow and view it in the observability UI
  3. Verify that custom class instances are displayed with their class names and serialized data
  4. Check that class references are shown as <class:ClassName> in the UI

Why make this change?

Previously, custom class instances couldn't be properly displayed in the observability UI because they weren't registered for deserialization in the o11y context. This change improves the developer experience by providing meaningful representations of custom classes and instances, making it easier to debug and understand workflow execution without requiring class registration.

@changeset-bot

changeset-botBot commented Jan 19, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: c889dd1

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 13 packages
NameType
@workflow/web-sharedPatch
@workflow/corePatch
@workflow/buildersPatch
@workflow/cliPatch
@workflow/docs-typecheckPatch
@workflow/nextPatch
@workflow/nitroPatch
workflowPatch
@workflow/astroPatch
@workflow/sveltekitPatch
@workflow/world-testingPatch
@workflow/nuxtPatch
@workflow/aiPatch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions

github-actionsBot commented Jan 19, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production435038473
❌ 💻 Local Development396232430
✅ 📦 Local Production398032430
✅ 🐘 Local Postgres398032430
✅ 🪟 Windows430043
❌ 🌍 Community Worlds162220184
Total1832241341990

❌ Failed Tests

💻 Local Development (2 failed)

sveltekit-stable (2 failed):

  • error handling error propagation step errors basic step error preserves message and stack trace
  • error handling error propagation step errors cross-file step error preserves message and function names in stack
🌍 Community Worlds (22 failed)

mongodb (1 failed):

  • webhookWorkflow

redis (1 failed):

  • webhookWorkflow

starter (19 failed):

  • addTenWorkflow
  • addTenWorkflow
  • error handling error propagation workflow errors nested function calls preserve message and stack trace
  • error handling error propagation workflow errors cross-file imports preserve message and stack trace
  • error handling error propagation step errors basic step error preserves message and stack trace
  • error handling error propagation step errors cross-file step error preserves message and function names in stack
  • error handling retry behavior regular Error retries until success
  • error handling retry behavior FatalError fails immediately without retries
  • error handling catchability FatalError can be caught and detected with FatalError.is()
  • hookCleanupTestWorkflow - hook token reuse after workflow completion
  • stepFunctionPassingWorkflow - step function references can be passed as arguments (without closure vars)
  • stepFunctionWithClosureWorkflow - step function with closure variables passed as argument
  • spawnWorkflowFromStepWorkflow - spawning a child workflow using start() inside a step
  • pathsAliasWorkflow - TypeScript path aliases resolve correctly
  • Calculator.calculate - static workflow method using static step methods from another class
  • AllInOneService.processNumber - static workflow method using sibling static step methods
  • ChainableService.processWithThis - static step methods using this to reference the class
  • thisSerializationWorkflow - step function invoked with .call() and .apply()
  • customSerializationWorkflow - custom class serialization with WORKFLOW_SERIALIZE/WORKFLOW_DESERIALIZE

turso (1 failed):

  • webhookWorkflow

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro3904
✅ example3904
✅ express3904
✅ fastify3904
✅ hono3904
✅ nextjs-turbopack4201
✅ nextjs-webpack4201
✅ nitro3904
✅ nuxt3904
✅ sveltekit3904
✅ vite3904
❌ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable3904
✅ express-stable3904
✅ fastify-stable3904
✅ hono-stable3904
✅ nextjs-turbopack-stable4300
✅ nextjs-webpack-stable4300
✅ nitro-stable3904
✅ nuxt-stable3904
❌ sveltekit-stable3724
✅ vite-stable3904
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable3904
✅ express-stable3904
✅ fastify-stable3904
✅ hono-stable3904
✅ nextjs-turbopack-stable4300
✅ nextjs-webpack-stable4300
✅ nitro-stable3904
✅ nuxt-stable3904
✅ sveltekit-stable3904
✅ vite-stable3904
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable3904
✅ express-stable3904
✅ fastify-stable3904
✅ hono-stable3904
✅ nextjs-turbopack-stable4300
✅ nextjs-webpack-stable4300
✅ nitro-stable3904
✅ nuxt-stable3904
✅ sveltekit-stable3904
✅ vite-stable3904
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack4300
❌ 🌍 Community Worlds
AppPassedFailedSkipped
✅ mongodb-dev300
❌ mongodb4210
✅ redis-dev300
❌ redis4210
✅ starter-dev300
❌ starter24190
✅ turso-dev300
❌ turso4210

📋 View full workflow run


Some E2E test jobs failed:

  • Vercel Prod: success
  • Local Dev: failure
  • Local Prod: success
  • Local Postgres: success
  • Windows: success

Check the workflow run for details.

@TooTallNateGraphite App

TooTallNate commented Jan 19, 2026

Copy link
Copy Markdown
MemberAuthor

@TooTallNate
TooTallNate marked this pull request as ready for review January 19, 2026 22:17
CopilotAI review requested due to automatic review settings January 19, 2026 22:17
@vercel

vercelBot commented Jan 19, 2026

Copy link
Copy Markdown
Contributor

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR enhances the observability UI to display custom class instances that cannot be fully deserialized because their classes are not registered in the o11y context. The implementation adds opaque markers for both class instances and class references, making it easier to debug workflows that use custom classes.

Changes:

  • Added CLASS_INSTANCE_REF_TYPE marker, ClassInstanceRef interface, and type guards to represent unregistered class instances
  • Implemented helper functions (extractClassName, serializedInstanceToRef, serializedClassToString) to convert serialized class data to display-friendly formats
  • Extended streamPrintRevivers to handle Instance and Class types by converting them to opaque markers
  • Updated UI transformation logic to display class instances inline with a __class__ marker alongside their serialized data

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 8 comments.

FileDescription
packages/core/src/observability.tsAdds ClassInstanceRef types, helper functions for class name extraction, and stream print revivers for Instance and Class types
packages/web-shared/src/sidebar/attribute-panel.tsxDuplicates ClassInstanceRef types for client-side use and updates transformValueForDisplay to handle class instance refs inline with class marker
.changeset/forty-tables-lick.mdDocuments the patch-level changes for both packages

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

'__type' in value &&
value.__type === CLASS_INSTANCE_REF_TYPE &&
'className' in value &&
typeof value.className === 'string'

CopilotAIJan 19, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The type guard isClassInstanceRef validates className but not classId. For consistency with isStreamRef which validates all required fields, and to ensure type safety, the function should also validate that classId exists and is a string.

Suggested change
typeofvalue.className==='string'
typeofvalue.className==='string'&&
'classId'invalue&&
typeofvalue.classId==='string'

Copilot uses AI. Check for mistakes.
Comment on lines +124 to +128
const extractClassName = (classId: string): string => {
if (!classId) return 'Unknown';
const parts = classId.split('/');
return parts[parts.length - 1] || classId;
};

CopilotAIJan 19, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The extractClassName function returns 'Unknown' when classId is falsy, but an empty string is falsy in JavaScript. When classId is an empty string, the function would return 'Unknown' without trying to split it. However, if the split produces an empty array or the last part is an empty string, it would fall back to returning the original classId. Consider handling the empty string case explicitly before the split operation to ensure consistent behavior.

Copilot uses AI. Check for mistakes.
Comment on lines +244 to +249
return {
__class__: v.className,
...(typeof v.data === 'object' && v.data !== null
? v.data
: { value: v.data }),
};

CopilotAIJan 19, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The spread operator on line 247 could potentially overwrite the class property if v.data is an object that contains a class key. This would result in the class name information being lost. Consider using a different marker name that's less likely to collide with actual object properties, or ensure the class property cannot be overwritten by spreading v.data after it instead of before.

Suggested change
return{
__class__: v.className,
...(typeofv.data==='object'&&v.data!==null
? v.data
: {value: v.data}),
};
returntypeofv.data==='object'&&v.data!==null
? {
...(v.dataasRecord<string,unknown>),
__class__: v.className,
}
: {
value: v.data,
__class__: v.className,
};

Copilot uses AI. Check for mistakes.
Comment on lines +244 to +248
return {
__class__: v.className,
...(typeof v.data === 'object' && v.data !== null
? v.data
: { value: v.data }),

CopilotAIJan 19, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The v.data object is spread directly without being transformed recursively. If v.data contains nested StreamRef or ClassInstanceRef objects, they won't be properly handled. The nested data should be recursively transformed before spreading to ensure all special references are properly converted.

Suggested change
return{
__class__: v.className,
...(typeofv.data==='object'&&v.data!==null
? v.data
: {value: v.data}),
consttransformedData=
typeofv.data==='object'&&v.data!==null
? transform(v.data)
: transform(v.data);
return{
__class__: v.className,
...(typeofv.data==='object'&&v.data!==null
? (transformedDataasRecord<string,unknown>)
: {value: transformedData}),

Copilot uses AI. Check for mistakes.
Comment on lines +151 to 157
const serializedClassToString = (value: { classId: string }): string => {
const className = extractClassName(value.classId);
return `<class:${className}>`;
};

/**
* This is an extra reviver for devalue that takes any streams that would be converted,

CopilotAIJan 19, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The serializedClassToString function doesn't validate that the input value has the expected structure. If value.classId is undefined or not a string, extractClassName will handle it, but it would be clearer to add explicit validation to ensure the function is called with the expected shape of data.

Suggested change
constserializedClassToString=(value: {classId: string}): string=>{
constclassName=extractClassName(value.classId);
return`<class:${className}>`;
};
/**
*Thisisanextrareviverfordevaluethattakesanystreamsthatwouldbeconverted,
*Convertaserializedclassreferencetoa string representation.
*ThisisusedforClasstype(theconstructorreferenceitself,notaninstance).
*/
constserializedClassToString=(value: unknown): string=>{
if(
!value||
typeofvalue!=='object'||
!('classId'invalue)||
typeof(valueas{classId: unknown}).classId!=='string'
){
return'<class:Unknown>';
}
constclassId=(valueas{classId: string}).classId;
constclassName=extractClassName(classId);
return`<class:${className}>`;
};

Copilot uses AI. Check for mistakes.
Comment threadpackages/core/src/observability.ts Outdated
Comment on lines +139 to +143
return {
__type: CLASS_INSTANCE_REF_TYPE,
className: extractClassName(value.classId),
classId: value.classId,
data: value.data,

CopilotAIJan 19, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The serializedInstanceToRef function doesn't validate that the input value has the expected structure. While the type annotation suggests the expected shape, adding runtime validation would make the function more robust, especially since it's called from the streamPrintRevivers which receives values of type any.

Suggested change
return{
__type: CLASS_INSTANCE_REF_TYPE,
className: extractClassName(value.classId),
classId: value.classId,
data: value.data,
// Runtime validation because this function is ultimately called with values of type `any`.
constisObject=value!==null&&typeofvalue==='object';
constclassId=isObject&&typeof(valueasany).classId==='string'
? (valueasany).classId
: '';
constdata=isObject ? (valueasany).data : value;
return{
__type: CLASS_INSTANCE_REF_TYPE,
className: extractClassName(classId),
classId,
data,

Copilot uses AI. Check for mistakes.
Comment on lines +120 to +154
/**
* Extract the class name from a classId.
* The classId format is typically "path/to/file/ClassName" so we extract the last segment.
*/
const extractClassName = (classId: string): string => {
if (!classId) return 'Unknown';
const parts = classId.split('/');
return parts[parts.length - 1] || classId;
};

/**
* Convert a serialized class instance to a ClassInstanceRef for o11y display.
* This allows viewing custom class instances in the UI without needing
* the class to be registered for deserialization.
*/
const serializedInstanceToRef = (value: {
classId: string;
data: unknown;
}): ClassInstanceRef => {
return {
__type: CLASS_INSTANCE_REF_TYPE,
className: extractClassName(value.classId),
classId: value.classId,
data: value.data,
};
};

/**
* Convert a serialized class reference to a string representation.
* This is used for Class type (the constructor reference itself, not an instance).
*/
const serializedClassToString = (value: { classId: string }): string => {
const className = extractClassName(value.classId);
return `<class:${className}>`;
};

CopilotAIJan 19, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new helper functions extractClassName, serializedInstanceToRef, and serializedClassToString lack test coverage. Given that other utility functions in the codebase have comprehensive test files (e.g., serialization.test.ts, parse-name.test.ts), consider adding tests to verify the behavior of these functions, especially edge cases like empty classId, classIds with different path separators, and handling of various data types.

Copilot uses AI. Check for mistakes.
Comment threadpackages/core/src/observability.ts Outdated
'__type' in value &&
value.__type === CLASS_INSTANCE_REF_TYPE &&
'className' in value &&
typeof value.className === 'string'

CopilotAIJan 19, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The type guard isClassInstanceRef validates className but not classId. For consistency with isStreamRef which validates all required fields, and to ensure type safety, the function should also validate that classId exists and is a string. This would prevent runtime errors if a malformed ClassInstanceRef object is encountered.

Suggested change
typeofvalue.className==='string'
typeofvalue.className==='string'&&
'classId'invalue&&
typeofvalue.classId==='string'

Copilot uses AI. Check for mistakes.
@TooTallNate
TooTallNateforce-pushed the 01-19-show_opaque_marker_when_deserializing_custom_classes_and_instances_in_o11y branch from 127cfec to c46327fCompareJanuary 19, 2026 22:26
@TooTallNate
TooTallNateforce-pushed the 01-09-add_support_for_custom_class_instance_serialization branch from baea85a to 4216347CompareJanuary 19, 2026 22:26
value: unknown
): { json: string; streamRefs: Map<string, StreamRef> } => {
): {
json: string;

@vercelvercelBotJan 19, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ClassInstanceRef transformation allows properties in v.data to overwrite the class marker, and doesn't recursively transform nested ClassInstanceRef objects within v.data

Fix on Vercel

* Used in o11y when a custom class instance is encountered but the class is not
* registered for deserialization.
*/
interface ClassInstanceRef {

@vercelvercelBotJan 19, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Type guard isClassInstanceRef doesn't validate classId and data properties required by ClassInstanceRef interface, leading to type unsafety

Fix on Vercel

Comment threadpackages/core/src/observability.ts Outdated
'__type' in value &&
value.__type === CLASS_INSTANCE_REF_TYPE &&
'className' in value &&
typeof value.className === 'string'

@vercelvercelBotJan 19, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The isClassInstanceRef type guard doesn't validate classId and data properties required by the ClassInstanceRef interface

Fix on Vercel

@TooTallNate
TooTallNateforce-pushed the 01-19-show_opaque_marker_when_deserializing_custom_classes_and_instances_in_o11y branch from a14fe6d to 04b36f5CompareJanuary 19, 2026 23:14
@github-actions

github-actionsBot commented Jan 19, 2026

Copy link
Copy Markdown
Contributor

📊 Benchmark Results

📈 Comparing against baseline from main branch. Green 🟢 = faster, Red 🔺 = slower.

workflow with no steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Starter🥇 Next.js (Turbopack)0.040s (-1.2%)1.014s (~)0.974s101.00x
💻 LocalNext.js (Turbopack)0.042s (+2.7%)1.019s (~)0.977s101.06x
💻 LocalNitro0.043s (-3.3%)1.007s (~)0.964s101.09x
💻 LocalExpress0.044s (-1.1%)1.007s (~)0.963s101.11x
🌐 RedisNext.js (Turbopack)0.045s (+11.9% 🔺)1.018s (~)0.973s101.14x
🌐 TursoNext.js (Turbopack)0.106s (-3.1%)1.013s (~)0.908s102.66x
🌐 MongoDBNext.js (Turbopack)0.122s (+123.6% 🔺)1.018s (~)0.895s103.08x
🐘 PostgresNext.js (Turbopack)0.176s (-44.7% 🟢)1.022s (~)0.845s104.44x
🐘 PostgresNitro0.337s (+6.8% 🔺)1.012s (~)0.675s108.49x
🐘 PostgresExpress0.338s (+5.0% 🔺)1.013s (~)0.675s108.52x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro0.575s (-80.7% 🟢)1.580s (-58.0% 🟢)1.005s101.00x
▲ VercelExpress0.689s (-83.2% 🟢)1.686s (-66.5% 🟢)0.997s101.20x
▲ VercelNext.js (Turbopack)0.744s (-66.1% 🟢)1.568s (-45.5% 🟢)0.824s101.29x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Starter🥇 Next.js (Turbopack)1.090s (~)2.010s (~)0.920s101.00x
💻 LocalNext.js (Turbopack)1.098s (+0.6%)2.012s (~)0.914s101.01x
💻 LocalExpress1.112s (~)2.006s (~)0.895s101.02x
🌐 RedisNext.js (Turbopack)1.113s (+0.8%)2.013s (~)0.900s101.02x
💻 LocalNitro1.115s (~)2.007s (~)0.892s101.02x
🌐 MongoDBNext.js (Turbopack)1.261s (-4.0%)2.016s (~)0.755s101.16x
🌐 TursoNext.js (Turbopack)1.305s (~)2.013s (~)0.707s101.20x
🐘 PostgresExpress2.185s (+1.0%)3.015s (~)0.830s102.00x
🐘 PostgresNitro2.194s (+0.7%)3.015s (~)0.821s102.01x
🐘 PostgresNext.js (Turbopack)2.322s (+35.2% 🔺)2.818s (+33.2% 🔺)0.496s102.13x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)2.682s (-67.8% 🟢)3.592s (-59.7% 🟢)0.910s101.00x
▲ VercelExpress2.694s (-60.3% 🟢)3.718s (-51.6% 🟢)1.024s101.00x
▲ VercelNitro2.712s (-61.9% 🟢)3.698s (-53.0% 🟢)0.987s101.01x

🔍 Observability: Next.js (Turbopack) | Express | Nitro

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Starter🥇 Next.js (Turbopack)10.606s (~)11.012s (~)0.406s51.00x
💻 LocalNext.js (Turbopack)10.664s (~)11.014s (~)0.350s51.01x
🌐 RedisNext.js (Turbopack)10.708s (~)11.021s (~)0.314s51.01x
💻 LocalExpress10.787s (~)11.010s (~)0.223s51.02x
💻 LocalNitro10.789s (~)11.009s (~)0.220s51.02x
🌐 MongoDBNext.js (Turbopack)12.119s (-1.1%)12.829s (-1.5%)0.710s51.14x
🌐 TursoNext.js (Turbopack)12.175s (~)13.020s (~)0.845s51.15x
🐘 PostgresNext.js (Turbopack)15.410s (+2.7%)15.838s (+1.3%)0.428s51.45x
🐘 PostgresNitro20.351s (~)21.029s (~)0.678s51.92x
🐘 PostgresExpress20.416s (~)21.034s (~)0.618s51.92x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express21.836s (-1.2%)22.549s (-1.4%)0.713s51.00x
▲ VercelNext.js (Turbopack)22.144s (-0.7%)22.815s (~)0.671s51.01x
▲ VercelNitro22.426s (+2.0%)23.192s (+1.9%)0.767s51.03x

🔍 Observability: Express | Next.js (Turbopack) | Nitro

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Starter🥇 Next.js (Turbopack)1.355s (~)2.008s (~)0.653s151.00x
🌐 RedisNext.js (Turbopack)1.374s (+1.7%)2.011s (~)0.637s151.01x
💻 LocalNext.js (Turbopack)1.392s (~)2.013s (~)0.621s151.03x
💻 LocalNitro1.417s (+0.9%)2.005s (~)0.588s151.05x
💻 LocalExpress1.420s (+0.6%)2.007s (~)0.587s151.05x
🐘 PostgresNext.js (Turbopack)1.877s (+5.9% 🔺)2.014s (~)0.136s151.39x
🌐 MongoDBNext.js (Turbopack)2.144s (+1.0%)3.014s (~)0.870s101.58x
🌐 TursoNext.js (Turbopack)2.222s (+1.3%)3.012s (~)0.790s101.64x
🐘 PostgresExpress2.363s (~)3.012s (~)0.649s101.74x
🐘 PostgresNitro2.487s (+3.1%)3.012s (~)0.525s101.84x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.936s (-8.1% 🟢)3.789s (-5.6% 🟢)0.853s81.00x
▲ VercelExpress3.069s (+6.4% 🔺)3.925s (+7.8% 🔺)0.856s81.05x
▲ VercelNext.js (Turbopack)3.214s (+7.1% 🔺)3.982s (+6.0% 🔺)0.768s81.09x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)2.040s (-1.4%)2.896s (+3.9%)0.855s111.00x
💻 LocalExpress2.208s (~)3.181s (~)0.973s101.08x
💻 LocalNitro2.241s (+1.4%)3.168s (~)0.928s101.10x
🌐 StarterNext.js (Turbopack)2.483s (~)3.008s (~)0.525s101.22x
🌐 RedisNext.js (Turbopack)2.492s (+0.6%)3.012s (-0.7%)0.520s101.22x
🐘 PostgresNext.js (Turbopack)2.550s (-5.2% 🟢)3.020s (-0.8%)0.470s101.25x
🐘 PostgresExpress2.876s (-6.9% 🟢)3.232s (-17.3% 🟢)0.356s101.41x
🐘 PostgresNitro2.955s (~)3.114s (-7.0% 🟢)0.159s101.45x
🌐 MongoDBNext.js (Turbopack)4.650s (-0.6%)5.185s (~)0.535s62.28x
🌐 TursoNext.js (Turbopack)4.851s (+1.7%)5.180s (~)0.329s62.38x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.997s (+11.6% 🔺)4.820s (+16.3% 🔺)0.823s71.00x
▲ VercelExpress4.022s (+28.6% 🔺)4.751s (+23.7% 🔺)0.729s71.01x
▲ VercelNext.js (Turbopack)4.660s (+40.1% 🔺)5.415s (+38.1% 🔺)0.755s61.17x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Starter🥇 Next.js (Turbopack)1.359s (+0.5%)2.007s (~)0.648s151.00x
🌐 RedisNext.js (Turbopack)1.367s (~)2.011s (~)0.644s151.01x
💻 LocalExpress1.406s (-1.4%)2.006s (~)0.600s151.03x
💻 LocalNext.js (Turbopack)1.409s (+1.3%)2.015s (~)0.605s151.04x
💻 LocalNitro1.428s (~)2.005s (~)0.577s151.05x
🐘 PostgresNitro1.557s (-23.4% 🟢)2.010s (-16.1% 🟢)0.452s151.15x
🐘 PostgresNext.js (Turbopack)1.676s (-25.9% 🟢)2.013s (-25.2% 🟢)0.337s151.23x
🐘 PostgresExpress1.836s (+3.1%)2.010s (-3.3%)0.174s151.35x
🌐 MongoDBNext.js (Turbopack)2.116s (-0.9%)3.019s (~)0.903s101.56x
🌐 TursoNext.js (Turbopack)2.219s (-1.2%)3.013s (~)0.794s101.63x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)2.837s (+2.4%)3.571s (-2.7%)0.735s91.00x
▲ VercelExpress3.139s (+7.3% 🔺)4.119s (+10.8% 🔺)0.980s81.11x
▲ VercelNitro3.218s (-5.2% 🟢)4.497s (+2.9%)1.279s71.13x

🔍 Observability: Next.js (Turbopack) | Express | Nitro

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express2.235s (+0.7%)3.197s (+0.6%)0.963s101.00x
💻 LocalNext.js (Turbopack)2.307s (+8.1% 🔺)3.142s (+3.7%)0.835s101.03x
💻 LocalNitro2.378s (+3.5%)3.317s (+2.9%)0.939s101.06x
🌐 StarterNext.js (Turbopack)2.459s (~)3.008s (~)0.549s101.10x
🐘 PostgresNitro2.497s (-18.3% 🟢)3.017s (-15.7% 🟢)0.520s101.12x
🌐 RedisNext.js (Turbopack)2.517s (+1.3%)3.012s (~)0.495s101.13x
🐘 PostgresNext.js (Turbopack)2.583s (-5.0%)3.029s (-0.8%)0.447s101.16x
🐘 PostgresExpress2.642s (-8.9% 🟢)3.018s (-6.5% 🟢)0.377s101.18x
🌐 MongoDBNext.js (Turbopack)4.711s (~)5.181s (~)0.470s62.11x
🌐 TursoNext.js (Turbopack)4.778s (+1.6%)5.183s (-3.1%)0.405s62.14x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.209s (+2.5%)3.778s (+0.8%)0.569s81.00x
▲ VercelExpress3.254s (~)3.848s (+2.4%)0.594s81.01x
▲ VercelNext.js (Turbopack)3.454s (+0.9%)4.032s (~)0.578s81.08x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🌐 Starter🥇 Next.js (Turbopack)0.129s (~)1.005s (~)0.000s (NaN%)1.011s (~)0.882s101.00x
💻 LocalNext.js (Turbopack)0.141s (-3.4%)1.003s (~)0.017s (-0.6%)1.028s (~)0.887s101.09x
🌐 RedisNext.js (Turbopack)0.159s (+11.6% 🔺)1.005s (~)0.000s (-100.0% 🟢)1.015s (~)0.855s101.23x
💻 LocalExpress0.176s (-0.7%)0.992s (~)0.015s (-1.9%)1.022s (~)0.846s101.36x
💻 LocalNitro0.182s (+1.7%)0.992s (~)0.015s (-0.7%)1.021s (~)0.838s101.41x
🌐 TursoNext.js (Turbopack)0.488s (+6.3% 🔺)0.965s (-3.0%)0.000s (+Infinity% 🔺)1.013s (~)0.524s103.78x
🌐 MongoDBNext.js (Turbopack)0.506s (-3.3%)0.941s (+1.7%)0.000s (+Infinity% 🔺)1.015s (~)0.509s103.92x
🐘 PostgresNext.js (Turbopack)0.673s (-29.9% 🟢)1.052s (-1.5%)0.000s (-100.0% 🟢)1.118s (-15.1% 🟢)0.444s105.21x
🐘 PostgresExpress2.313s (+3.0%)2.728s (-2.7%)0.000s (-100.0% 🟢)3.014s (~)0.700s1017.90x
🐘 PostgresNitro2.402s (+3.2%)2.639s (-2.9%)0.000s (+Infinity% 🔺)3.014s (~)0.611s1018.59x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)2.826s (~)3.323s (+6.2% 🔺)0.378s (-49.5% 🟢)4.114s (-4.7%)1.288s101.00x
▲ VercelNitro2.891s (-0.7%)3.301s (+3.4%)0.454s (-23.0% 🟢)4.226s (-0.6%)1.335s101.02x
▲ VercelExpress2.914s (+1.5%)3.295s (+3.9%)0.396s (-58.0% 🟢)4.190s (-8.4% 🟢)1.276s101.03x

🔍 Observability: Next.js (Turbopack) | Nitro | Express

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalNext.js (Turbopack)6/8
🐘 PostgresNext.js (Turbopack)5/8
▲ VercelNitro4/8
Fastest World by Framework

Winner determined by most benchmark wins

Framework🥇 Fastest WorldWins
Express💻 Local8/8
Next.js (Turbopack)🌐 Starter6/8
Nitro💻 Local8/8
Column Definitions
  • Workflow Time: Runtime reported by workflow (completedAt - createdAt) - primary metric
  • TTFB: Time to First Byte - time from workflow start until first stream byte received (stream benchmarks only)
  • Slurp: Time from first byte to complete stream consumption (stream benchmarks only)
  • Wall Time: Total testbench time (trigger workflow + poll for result)
  • Overhead: Testbench overhead (Wall Time - Workflow Time)
  • Samples: Number of benchmark iterations run
  • vs Fastest: How much slower compared to the fastest configuration for this benchmark

Worlds:

  • 💻 Local: In-memory filesystem world (local development)
  • 🐘 Postgres: PostgreSQL database world (local development)
  • ▲ Vercel: Vercel production/preview deployment
  • 🌐 Starter: Community world (local development)
  • 🌐 Turso: Community world (local development)
  • 🌐 MongoDB: Community world (local development)
  • 🌐 Redis: Community world (local development)
  • 🌐 Jazz: Community world (local development)

📋 View full workflow run

@VaguelySeriousVaguelySerious left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM ✨

One styling change:

className="inline-flex flex-col rounded text-[11px] font-mono my-1"
style={{
backgroundColor: colors.body,
border: `1px solid ${colors.header}`,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This leaves some weird extra white pixel borders

Suggested change
border: `1px solid ${colors.header}`,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually I tested too early, this seems to already be resolved

@socket-security

socket-securityBot commented Jan 20, 2026

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

DiffPackageSupply Chain
Security
VulnerabilityQualityMaintenanceLicense
Addednpm/​@​types/​color-hash@​2.0.01001009080100
Addednpm/​color-hash@​2.0.210010010080100

View full report

Comment thread.changeset/forty-tables-lick.md Outdated
TooTallNateand others added 2 commits January 20, 2026 01:39
Co-authored-by: Peter Wielander <mittgfu@gmail.com>
Signed-off-by: Nathan Rajlich <n@n8.io>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@TooTallNate@VaguelySerious
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); Show opaque marker when deserializing custom classes and instances in o11y by TooTallNate · Pull Request #809 · vercel/workflow · GitHub
Skip to content

Show opaque marker when deserializing custom classes and instances in o11y - #809

Merged
TooTallNate merged 8 commits into
mainfrom
01-19-show_opaque_marker_when_deserializing_custom_classes_and_instances_in_o11y
Jan 20, 2026
Merged

Show opaque marker when deserializing custom classes and instances in o11y#809
TooTallNate merged 8 commits into
mainfrom
01-19-show_opaque_marker_when_deserializing_custom_classes_and_instances_in_o11y

Conversation

@TooTallNate

@TooTallNateTooTallNate commented Jan 19, 2026

Copy link
Copy Markdown
Member

Web example:

dark mode
Screenshot 2026-01-20 at 01 38 04

light mode
Screenshot 2026-01-20 at 01 38 09

CLI example:

Screenshot 2026-01-20 at 01 44 53

Added support for displaying custom class instances in the observability UI by showing opaque markers when deserializing custom classes.

What changed?

  • Added a CLASS_INSTANCE_REF_TYPE marker and related interfaces to represent serialized class instances that cannot be fully deserialized
  • Implemented helper functions to extract class names from class IDs and convert serialized instances to reference objects
  • Enhanced the stream print revivers to handle custom class instances and class references
  • Updated the attribute panel to recognize and display class instance references with a __class__ marker alongside the serialized data

How to test?

  1. Create a workflow that uses custom classes
  2. Run the workflow and view it in the observability UI
  3. Verify that custom class instances are displayed with their class names and serialized data
  4. Check that class references are shown as <class:ClassName> in the UI

Why make this change?

Previously, custom class instances couldn't be properly displayed in the observability UI because they weren't registered for deserialization in the o11y context. This change improves the developer experience by providing meaningful representations of custom classes and instances, making it easier to debug and understand workflow execution without requiring class registration.

@changeset-bot

changeset-botBot commented Jan 19, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: c889dd1

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 13 packages
NameType
@workflow/web-sharedPatch
@workflow/corePatch
@workflow/buildersPatch
@workflow/cliPatch
@workflow/docs-typecheckPatch
@workflow/nextPatch
@workflow/nitroPatch
workflowPatch
@workflow/astroPatch
@workflow/sveltekitPatch
@workflow/world-testingPatch
@workflow/nuxtPatch
@workflow/aiPatch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions

github-actionsBot commented Jan 19, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production435038473
❌ 💻 Local Development396232430
✅ 📦 Local Production398032430
✅ 🐘 Local Postgres398032430
✅ 🪟 Windows430043
❌ 🌍 Community Worlds162220184
Total1832241341990

❌ Failed Tests

💻 Local Development (2 failed)

sveltekit-stable (2 failed):

  • error handling error propagation step errors basic step error preserves message and stack trace
  • error handling error propagation step errors cross-file step error preserves message and function names in stack
🌍 Community Worlds (22 failed)

mongodb (1 failed):

  • webhookWorkflow

redis (1 failed):

  • webhookWorkflow

starter (19 failed):

  • addTenWorkflow
  • addTenWorkflow
  • error handling error propagation workflow errors nested function calls preserve message and stack trace
  • error handling error propagation workflow errors cross-file imports preserve message and stack trace
  • error handling error propagation step errors basic step error preserves message and stack trace
  • error handling error propagation step errors cross-file step error preserves message and function names in stack
  • error handling retry behavior regular Error retries until success
  • error handling retry behavior FatalError fails immediately without retries
  • error handling catchability FatalError can be caught and detected with FatalError.is()
  • hookCleanupTestWorkflow - hook token reuse after workflow completion
  • stepFunctionPassingWorkflow - step function references can be passed as arguments (without closure vars)
  • stepFunctionWithClosureWorkflow - step function with closure variables passed as argument
  • spawnWorkflowFromStepWorkflow - spawning a child workflow using start() inside a step
  • pathsAliasWorkflow - TypeScript path aliases resolve correctly
  • Calculator.calculate - static workflow method using static step methods from another class
  • AllInOneService.processNumber - static workflow method using sibling static step methods
  • ChainableService.processWithThis - static step methods using this to reference the class
  • thisSerializationWorkflow - step function invoked with .call() and .apply()
  • customSerializationWorkflow - custom class serialization with WORKFLOW_SERIALIZE/WORKFLOW_DESERIALIZE

turso (1 failed):

  • webhookWorkflow

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro3904
✅ example3904
✅ express3904
✅ fastify3904
✅ hono3904
✅ nextjs-turbopack4201
✅ nextjs-webpack4201
✅ nitro3904
✅ nuxt3904
✅ sveltekit3904
✅ vite3904
❌ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable3904
✅ express-stable3904
✅ fastify-stable3904
✅ hono-stable3904
✅ nextjs-turbopack-stable4300
✅ nextjs-webpack-stable4300
✅ nitro-stable3904
✅ nuxt-stable3904
❌ sveltekit-stable3724
✅ vite-stable3904
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable3904
✅ express-stable3904
✅ fastify-stable3904
✅ hono-stable3904
✅ nextjs-turbopack-stable4300
✅ nextjs-webpack-stable4300
✅ nitro-stable3904
✅ nuxt-stable3904
✅ sveltekit-stable3904
✅ vite-stable3904
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable3904
✅ express-stable3904
✅ fastify-stable3904
✅ hono-stable3904
✅ nextjs-turbopack-stable4300
✅ nextjs-webpack-stable4300
✅ nitro-stable3904
✅ nuxt-stable3904
✅ sveltekit-stable3904
✅ vite-stable3904
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack4300
❌ 🌍 Community Worlds
AppPassedFailedSkipped
✅ mongodb-dev300
❌ mongodb4210
✅ redis-dev300
❌ redis4210
✅ starter-dev300
❌ starter24190
✅ turso-dev300
❌ turso4210

📋 View full workflow run


Some E2E test jobs failed:

  • Vercel Prod: success
  • Local Dev: failure
  • Local Prod: success
  • Local Postgres: success
  • Windows: success

Check the workflow run for details.

@TooTallNateGraphite App

TooTallNate commented Jan 19, 2026

Copy link
Copy Markdown
MemberAuthor

@TooTallNate
TooTallNate marked this pull request as ready for review January 19, 2026 22:17
CopilotAI review requested due to automatic review settings January 19, 2026 22:17
@vercel

vercelBot commented Jan 19, 2026

Copy link
Copy Markdown
Contributor

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR enhances the observability UI to display custom class instances that cannot be fully deserialized because their classes are not registered in the o11y context. The implementation adds opaque markers for both class instances and class references, making it easier to debug workflows that use custom classes.

Changes:

  • Added CLASS_INSTANCE_REF_TYPE marker, ClassInstanceRef interface, and type guards to represent unregistered class instances
  • Implemented helper functions (extractClassName, serializedInstanceToRef, serializedClassToString) to convert serialized class data to display-friendly formats
  • Extended streamPrintRevivers to handle Instance and Class types by converting them to opaque markers
  • Updated UI transformation logic to display class instances inline with a __class__ marker alongside their serialized data

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 8 comments.

FileDescription
packages/core/src/observability.tsAdds ClassInstanceRef types, helper functions for class name extraction, and stream print revivers for Instance and Class types
packages/web-shared/src/sidebar/attribute-panel.tsxDuplicates ClassInstanceRef types for client-side use and updates transformValueForDisplay to handle class instance refs inline with class marker
.changeset/forty-tables-lick.mdDocuments the patch-level changes for both packages

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

'__type' in value &&
value.__type === CLASS_INSTANCE_REF_TYPE &&
'className' in value &&
typeof value.className === 'string'

CopilotAIJan 19, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The type guard isClassInstanceRef validates className but not classId. For consistency with isStreamRef which validates all required fields, and to ensure type safety, the function should also validate that classId exists and is a string.

Suggested change
typeofvalue.className==='string'
typeofvalue.className==='string'&&
'classId'invalue&&
typeofvalue.classId==='string'

Copilot uses AI. Check for mistakes.
Comment on lines +124 to +128
const extractClassName = (classId: string): string => {
if (!classId) return 'Unknown';
const parts = classId.split('/');
return parts[parts.length - 1] || classId;
};

CopilotAIJan 19, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The extractClassName function returns 'Unknown' when classId is falsy, but an empty string is falsy in JavaScript. When classId is an empty string, the function would return 'Unknown' without trying to split it. However, if the split produces an empty array or the last part is an empty string, it would fall back to returning the original classId. Consider handling the empty string case explicitly before the split operation to ensure consistent behavior.

Copilot uses AI. Check for mistakes.
Comment on lines +244 to +249
return {
__class__: v.className,
...(typeof v.data === 'object' && v.data !== null
? v.data
: { value: v.data }),
};

CopilotAIJan 19, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The spread operator on line 247 could potentially overwrite the class property if v.data is an object that contains a class key. This would result in the class name information being lost. Consider using a different marker name that's less likely to collide with actual object properties, or ensure the class property cannot be overwritten by spreading v.data after it instead of before.

Suggested change
return{
__class__: v.className,
...(typeofv.data==='object'&&v.data!==null
? v.data
: {value: v.data}),
};
returntypeofv.data==='object'&&v.data!==null
? {
...(v.dataasRecord<string,unknown>),
__class__: v.className,
}
: {
value: v.data,
__class__: v.className,
};

Copilot uses AI. Check for mistakes.
Comment on lines +244 to +248
return {
__class__: v.className,
...(typeof v.data === 'object' && v.data !== null
? v.data
: { value: v.data }),

CopilotAIJan 19, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The v.data object is spread directly without being transformed recursively. If v.data contains nested StreamRef or ClassInstanceRef objects, they won't be properly handled. The nested data should be recursively transformed before spreading to ensure all special references are properly converted.

Suggested change
return{
__class__: v.className,
...(typeofv.data==='object'&&v.data!==null
? v.data
: {value: v.data}),
consttransformedData=
typeofv.data==='object'&&v.data!==null
? transform(v.data)
: transform(v.data);
return{
__class__: v.className,
...(typeofv.data==='object'&&v.data!==null
? (transformedDataasRecord<string,unknown>)
: {value: transformedData}),

Copilot uses AI. Check for mistakes.
Comment on lines +151 to 157
const serializedClassToString = (value: { classId: string }): string => {
const className = extractClassName(value.classId);
return `<class:${className}>`;
};

/**
* This is an extra reviver for devalue that takes any streams that would be converted,

CopilotAIJan 19, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The serializedClassToString function doesn't validate that the input value has the expected structure. If value.classId is undefined or not a string, extractClassName will handle it, but it would be clearer to add explicit validation to ensure the function is called with the expected shape of data.

Suggested change
constserializedClassToString=(value: {classId: string}): string=>{
constclassName=extractClassName(value.classId);
return`<class:${className}>`;
};
/**
*Thisisanextrareviverfordevaluethattakesanystreamsthatwouldbeconverted,
*Convertaserializedclassreferencetoa string representation.
*ThisisusedforClasstype(theconstructorreferenceitself,notaninstance).
*/
constserializedClassToString=(value: unknown): string=>{
if(
!value||
typeofvalue!=='object'||
!('classId'invalue)||
typeof(valueas{classId: unknown}).classId!=='string'
){
return'<class:Unknown>';
}
constclassId=(valueas{classId: string}).classId;
constclassName=extractClassName(classId);
return`<class:${className}>`;
};

Copilot uses AI. Check for mistakes.
Comment threadpackages/core/src/observability.ts Outdated
Comment on lines +139 to +143
return {
__type: CLASS_INSTANCE_REF_TYPE,
className: extractClassName(value.classId),
classId: value.classId,
data: value.data,

CopilotAIJan 19, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The serializedInstanceToRef function doesn't validate that the input value has the expected structure. While the type annotation suggests the expected shape, adding runtime validation would make the function more robust, especially since it's called from the streamPrintRevivers which receives values of type any.

Suggested change
return{
__type: CLASS_INSTANCE_REF_TYPE,
className: extractClassName(value.classId),
classId: value.classId,
data: value.data,
// Runtime validation because this function is ultimately called with values of type `any`.
constisObject=value!==null&&typeofvalue==='object';
constclassId=isObject&&typeof(valueasany).classId==='string'
? (valueasany).classId
: '';
constdata=isObject ? (valueasany).data : value;
return{
__type: CLASS_INSTANCE_REF_TYPE,
className: extractClassName(classId),
classId,
data,

Copilot uses AI. Check for mistakes.
Comment on lines +120 to +154
/**
* Extract the class name from a classId.
* The classId format is typically "path/to/file/ClassName" so we extract the last segment.
*/
const extractClassName = (classId: string): string => {
if (!classId) return 'Unknown';
const parts = classId.split('/');
return parts[parts.length - 1] || classId;
};

/**
* Convert a serialized class instance to a ClassInstanceRef for o11y display.
* This allows viewing custom class instances in the UI without needing
* the class to be registered for deserialization.
*/
const serializedInstanceToRef = (value: {
classId: string;
data: unknown;
}): ClassInstanceRef => {
return {
__type: CLASS_INSTANCE_REF_TYPE,
className: extractClassName(value.classId),
classId: value.classId,
data: value.data,
};
};

/**
* Convert a serialized class reference to a string representation.
* This is used for Class type (the constructor reference itself, not an instance).
*/
const serializedClassToString = (value: { classId: string }): string => {
const className = extractClassName(value.classId);
return `<class:${className}>`;
};

CopilotAIJan 19, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new helper functions extractClassName, serializedInstanceToRef, and serializedClassToString lack test coverage. Given that other utility functions in the codebase have comprehensive test files (e.g., serialization.test.ts, parse-name.test.ts), consider adding tests to verify the behavior of these functions, especially edge cases like empty classId, classIds with different path separators, and handling of various data types.

Copilot uses AI. Check for mistakes.
Comment threadpackages/core/src/observability.ts Outdated
'__type' in value &&
value.__type === CLASS_INSTANCE_REF_TYPE &&
'className' in value &&
typeof value.className === 'string'

CopilotAIJan 19, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The type guard isClassInstanceRef validates className but not classId. For consistency with isStreamRef which validates all required fields, and to ensure type safety, the function should also validate that classId exists and is a string. This would prevent runtime errors if a malformed ClassInstanceRef object is encountered.

Suggested change
typeofvalue.className==='string'
typeofvalue.className==='string'&&
'classId'invalue&&
typeofvalue.classId==='string'

Copilot uses AI. Check for mistakes.
@TooTallNate
TooTallNateforce-pushed the 01-19-show_opaque_marker_when_deserializing_custom_classes_and_instances_in_o11y branch from 127cfec to c46327fCompareJanuary 19, 2026 22:26
@TooTallNate
TooTallNateforce-pushed the 01-09-add_support_for_custom_class_instance_serialization branch from baea85a to 4216347CompareJanuary 19, 2026 22:26
value: unknown
): { json: string; streamRefs: Map<string, StreamRef> } => {
): {
json: string;

@vercelvercelBotJan 19, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ClassInstanceRef transformation allows properties in v.data to overwrite the class marker, and doesn't recursively transform nested ClassInstanceRef objects within v.data

Fix on Vercel

* Used in o11y when a custom class instance is encountered but the class is not
* registered for deserialization.
*/
interface ClassInstanceRef {

@vercelvercelBotJan 19, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Type guard isClassInstanceRef doesn't validate classId and data properties required by ClassInstanceRef interface, leading to type unsafety

Fix on Vercel

Comment threadpackages/core/src/observability.ts Outdated
'__type' in value &&
value.__type === CLASS_INSTANCE_REF_TYPE &&
'className' in value &&
typeof value.className === 'string'

@vercelvercelBotJan 19, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The isClassInstanceRef type guard doesn't validate classId and data properties required by the ClassInstanceRef interface

Fix on Vercel

@TooTallNate
TooTallNateforce-pushed the 01-19-show_opaque_marker_when_deserializing_custom_classes_and_instances_in_o11y branch from a14fe6d to 04b36f5CompareJanuary 19, 2026 23:14
@github-actions

github-actionsBot commented Jan 19, 2026

Copy link
Copy Markdown
Contributor

📊 Benchmark Results

📈 Comparing against baseline from main branch. Green 🟢 = faster, Red 🔺 = slower.

workflow with no steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Starter🥇 Next.js (Turbopack)0.040s (-1.2%)1.014s (~)0.974s101.00x
💻 LocalNext.js (Turbopack)0.042s (+2.7%)1.019s (~)0.977s101.06x
💻 LocalNitro0.043s (-3.3%)1.007s (~)0.964s101.09x
💻 LocalExpress0.044s (-1.1%)1.007s (~)0.963s101.11x
🌐 RedisNext.js (Turbopack)0.045s (+11.9% 🔺)1.018s (~)0.973s101.14x
🌐 TursoNext.js (Turbopack)0.106s (-3.1%)1.013s (~)0.908s102.66x
🌐 MongoDBNext.js (Turbopack)0.122s (+123.6% 🔺)1.018s (~)0.895s103.08x
🐘 PostgresNext.js (Turbopack)0.176s (-44.7% 🟢)1.022s (~)0.845s104.44x
🐘 PostgresNitro0.337s (+6.8% 🔺)1.012s (~)0.675s108.49x
🐘 PostgresExpress0.338s (+5.0% 🔺)1.013s (~)0.675s108.52x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro0.575s (-80.7% 🟢)1.580s (-58.0% 🟢)1.005s101.00x
▲ VercelExpress0.689s (-83.2% 🟢)1.686s (-66.5% 🟢)0.997s101.20x
▲ VercelNext.js (Turbopack)0.744s (-66.1% 🟢)1.568s (-45.5% 🟢)0.824s101.29x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Starter🥇 Next.js (Turbopack)1.090s (~)2.010s (~)0.920s101.00x
💻 LocalNext.js (Turbopack)1.098s (+0.6%)2.012s (~)0.914s101.01x
💻 LocalExpress1.112s (~)2.006s (~)0.895s101.02x
🌐 RedisNext.js (Turbopack)1.113s (+0.8%)2.013s (~)0.900s101.02x
💻 LocalNitro1.115s (~)2.007s (~)0.892s101.02x
🌐 MongoDBNext.js (Turbopack)1.261s (-4.0%)2.016s (~)0.755s101.16x
🌐 TursoNext.js (Turbopack)1.305s (~)2.013s (~)0.707s101.20x
🐘 PostgresExpress2.185s (+1.0%)3.015s (~)0.830s102.00x
🐘 PostgresNitro2.194s (+0.7%)3.015s (~)0.821s102.01x
🐘 PostgresNext.js (Turbopack)2.322s (+35.2% 🔺)2.818s (+33.2% 🔺)0.496s102.13x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)2.682s (-67.8% 🟢)3.592s (-59.7% 🟢)0.910s101.00x
▲ VercelExpress2.694s (-60.3% 🟢)3.718s (-51.6% 🟢)1.024s101.00x
▲ VercelNitro2.712s (-61.9% 🟢)3.698s (-53.0% 🟢)0.987s101.01x

🔍 Observability: Next.js (Turbopack) | Express | Nitro

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Starter🥇 Next.js (Turbopack)10.606s (~)11.012s (~)0.406s51.00x
💻 LocalNext.js (Turbopack)10.664s (~)11.014s (~)0.350s51.01x
🌐 RedisNext.js (Turbopack)10.708s (~)11.021s (~)0.314s51.01x
💻 LocalExpress10.787s (~)11.010s (~)0.223s51.02x
💻 LocalNitro10.789s (~)11.009s (~)0.220s51.02x
🌐 MongoDBNext.js (Turbopack)12.119s (-1.1%)12.829s (-1.5%)0.710s51.14x
🌐 TursoNext.js (Turbopack)12.175s (~)13.020s (~)0.845s51.15x
🐘 PostgresNext.js (Turbopack)15.410s (+2.7%)15.838s (+1.3%)0.428s51.45x
🐘 PostgresNitro20.351s (~)21.029s (~)0.678s51.92x
🐘 PostgresExpress20.416s (~)21.034s (~)0.618s51.92x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express21.836s (-1.2%)22.549s (-1.4%)0.713s51.00x
▲ VercelNext.js (Turbopack)22.144s (-0.7%)22.815s (~)0.671s51.01x
▲ VercelNitro22.426s (+2.0%)23.192s (+1.9%)0.767s51.03x

🔍 Observability: Express | Next.js (Turbopack) | Nitro

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Starter🥇 Next.js (Turbopack)1.355s (~)2.008s (~)0.653s151.00x
🌐 RedisNext.js (Turbopack)1.374s (+1.7%)2.011s (~)0.637s151.01x
💻 LocalNext.js (Turbopack)1.392s (~)2.013s (~)0.621s151.03x
💻 LocalNitro1.417s (+0.9%)2.005s (~)0.588s151.05x
💻 LocalExpress1.420s (+0.6%)2.007s (~)0.587s151.05x
🐘 PostgresNext.js (Turbopack)1.877s (+5.9% 🔺)2.014s (~)0.136s151.39x
🌐 MongoDBNext.js (Turbopack)2.144s (+1.0%)3.014s (~)0.870s101.58x
🌐 TursoNext.js (Turbopack)2.222s (+1.3%)3.012s (~)0.790s101.64x
🐘 PostgresExpress2.363s (~)3.012s (~)0.649s101.74x
🐘 PostgresNitro2.487s (+3.1%)3.012s (~)0.525s101.84x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.936s (-8.1% 🟢)3.789s (-5.6% 🟢)0.853s81.00x
▲ VercelExpress3.069s (+6.4% 🔺)3.925s (+7.8% 🔺)0.856s81.05x
▲ VercelNext.js (Turbopack)3.214s (+7.1% 🔺)3.982s (+6.0% 🔺)0.768s81.09x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)2.040s (-1.4%)2.896s (+3.9%)0.855s111.00x
💻 LocalExpress2.208s (~)3.181s (~)0.973s101.08x
💻 LocalNitro2.241s (+1.4%)3.168s (~)0.928s101.10x
🌐 StarterNext.js (Turbopack)2.483s (~)3.008s (~)0.525s101.22x
🌐 RedisNext.js (Turbopack)2.492s (+0.6%)3.012s (-0.7%)0.520s101.22x
🐘 PostgresNext.js (Turbopack)2.550s (-5.2% 🟢)3.020s (-0.8%)0.470s101.25x
🐘 PostgresExpress2.876s (-6.9% 🟢)3.232s (-17.3% 🟢)0.356s101.41x
🐘 PostgresNitro2.955s (~)3.114s (-7.0% 🟢)0.159s101.45x
🌐 MongoDBNext.js (Turbopack)4.650s (-0.6%)5.185s (~)0.535s62.28x
🌐 TursoNext.js (Turbopack)4.851s (+1.7%)5.180s (~)0.329s62.38x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.997s (+11.6% 🔺)4.820s (+16.3% 🔺)0.823s71.00x
▲ VercelExpress4.022s (+28.6% 🔺)4.751s (+23.7% 🔺)0.729s71.01x
▲ VercelNext.js (Turbopack)4.660s (+40.1% 🔺)5.415s (+38.1% 🔺)0.755s61.17x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Starter🥇 Next.js (Turbopack)1.359s (+0.5%)2.007s (~)0.648s151.00x
🌐 RedisNext.js (Turbopack)1.367s (~)2.011s (~)0.644s151.01x
💻 LocalExpress1.406s (-1.4%)2.006s (~)0.600s151.03x
💻 LocalNext.js (Turbopack)1.409s (+1.3%)2.015s (~)0.605s151.04x
💻 LocalNitro1.428s (~)2.005s (~)0.577s151.05x
🐘 PostgresNitro1.557s (-23.4% 🟢)2.010s (-16.1% 🟢)0.452s151.15x
🐘 PostgresNext.js (Turbopack)1.676s (-25.9% 🟢)2.013s (-25.2% 🟢)0.337s151.23x
🐘 PostgresExpress1.836s (+3.1%)2.010s (-3.3%)0.174s151.35x
🌐 MongoDBNext.js (Turbopack)2.116s (-0.9%)3.019s (~)0.903s101.56x
🌐 TursoNext.js (Turbopack)2.219s (-1.2%)3.013s (~)0.794s101.63x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)2.837s (+2.4%)3.571s (-2.7%)0.735s91.00x
▲ VercelExpress3.139s (+7.3% 🔺)4.119s (+10.8% 🔺)0.980s81.11x
▲ VercelNitro3.218s (-5.2% 🟢)4.497s (+2.9%)1.279s71.13x

🔍 Observability: Next.js (Turbopack) | Express | Nitro

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express2.235s (+0.7%)3.197s (+0.6%)0.963s101.00x
💻 LocalNext.js (Turbopack)2.307s (+8.1% 🔺)3.142s (+3.7%)0.835s101.03x
💻 LocalNitro2.378s (+3.5%)3.317s (+2.9%)0.939s101.06x
🌐 StarterNext.js (Turbopack)2.459s (~)3.008s (~)0.549s101.10x
🐘 PostgresNitro2.497s (-18.3% 🟢)3.017s (-15.7% 🟢)0.520s101.12x
🌐 RedisNext.js (Turbopack)2.517s (+1.3%)3.012s (~)0.495s101.13x
🐘 PostgresNext.js (Turbopack)2.583s (-5.0%)3.029s (-0.8%)0.447s101.16x
🐘 PostgresExpress2.642s (-8.9% 🟢)3.018s (-6.5% 🟢)0.377s101.18x
🌐 MongoDBNext.js (Turbopack)4.711s (~)5.181s (~)0.470s62.11x
🌐 TursoNext.js (Turbopack)4.778s (+1.6%)5.183s (-3.1%)0.405s62.14x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.209s (+2.5%)3.778s (+0.8%)0.569s81.00x
▲ VercelExpress3.254s (~)3.848s (+2.4%)0.594s81.01x
▲ VercelNext.js (Turbopack)3.454s (+0.9%)4.032s (~)0.578s81.08x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🌐 Starter🥇 Next.js (Turbopack)0.129s (~)1.005s (~)0.000s (NaN%)1.011s (~)0.882s101.00x
💻 LocalNext.js (Turbopack)0.141s (-3.4%)1.003s (~)0.017s (-0.6%)1.028s (~)0.887s101.09x
🌐 RedisNext.js (Turbopack)0.159s (+11.6% 🔺)1.005s (~)0.000s (-100.0% 🟢)1.015s (~)0.855s101.23x
💻 LocalExpress0.176s (-0.7%)0.992s (~)0.015s (-1.9%)1.022s (~)0.846s101.36x
💻 LocalNitro0.182s (+1.7%)0.992s (~)0.015s (-0.7%)1.021s (~)0.838s101.41x
🌐 TursoNext.js (Turbopack)0.488s (+6.3% 🔺)0.965s (-3.0%)0.000s (+Infinity% 🔺)1.013s (~)0.524s103.78x
🌐 MongoDBNext.js (Turbopack)0.506s (-3.3%)0.941s (+1.7%)0.000s (+Infinity% 🔺)1.015s (~)0.509s103.92x
🐘 PostgresNext.js (Turbopack)0.673s (-29.9% 🟢)1.052s (-1.5%)0.000s (-100.0% 🟢)1.118s (-15.1% 🟢)0.444s105.21x
🐘 PostgresExpress2.313s (+3.0%)2.728s (-2.7%)0.000s (-100.0% 🟢)3.014s (~)0.700s1017.90x
🐘 PostgresNitro2.402s (+3.2%)2.639s (-2.9%)0.000s (+Infinity% 🔺)3.014s (~)0.611s1018.59x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)2.826s (~)3.323s (+6.2% 🔺)0.378s (-49.5% 🟢)4.114s (-4.7%)1.288s101.00x
▲ VercelNitro2.891s (-0.7%)3.301s (+3.4%)0.454s (-23.0% 🟢)4.226s (-0.6%)1.335s101.02x
▲ VercelExpress2.914s (+1.5%)3.295s (+3.9%)0.396s (-58.0% 🟢)4.190s (-8.4% 🟢)1.276s101.03x

🔍 Observability: Next.js (Turbopack) | Nitro | Express

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalNext.js (Turbopack)6/8
🐘 PostgresNext.js (Turbopack)5/8
▲ VercelNitro4/8
Fastest World by Framework

Winner determined by most benchmark wins

Framework🥇 Fastest WorldWins
Express💻 Local8/8
Next.js (Turbopack)🌐 Starter6/8
Nitro💻 Local8/8
Column Definitions
  • Workflow Time: Runtime reported by workflow (completedAt - createdAt) - primary metric
  • TTFB: Time to First Byte - time from workflow start until first stream byte received (stream benchmarks only)
  • Slurp: Time from first byte to complete stream consumption (stream benchmarks only)
  • Wall Time: Total testbench time (trigger workflow + poll for result)
  • Overhead: Testbench overhead (Wall Time - Workflow Time)
  • Samples: Number of benchmark iterations run
  • vs Fastest: How much slower compared to the fastest configuration for this benchmark

Worlds:

  • 💻 Local: In-memory filesystem world (local development)
  • 🐘 Postgres: PostgreSQL database world (local development)
  • ▲ Vercel: Vercel production/preview deployment
  • 🌐 Starter: Community world (local development)
  • 🌐 Turso: Community world (local development)
  • 🌐 MongoDB: Community world (local development)
  • 🌐 Redis: Community world (local development)
  • 🌐 Jazz: Community world (local development)

📋 View full workflow run

@VaguelySeriousVaguelySerious left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM ✨

One styling change:

className="inline-flex flex-col rounded text-[11px] font-mono my-1"
style={{
backgroundColor: colors.body,
border: `1px solid ${colors.header}`,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This leaves some weird extra white pixel borders

Suggested change
border: `1px solid ${colors.header}`,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually I tested too early, this seems to already be resolved

@socket-security

socket-securityBot commented Jan 20, 2026

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

DiffPackageSupply Chain
Security
VulnerabilityQualityMaintenanceLicense
Addednpm/​@​types/​color-hash@​2.0.01001009080100
Addednpm/​color-hash@​2.0.210010010080100

View full report

Comment thread.changeset/forty-tables-lick.md Outdated
TooTallNateand others added 2 commits January 20, 2026 01:39
Co-authored-by: Peter Wielander <mittgfu@gmail.com>
Signed-off-by: Nathan Rajlich <n@n8.io>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@TooTallNate@VaguelySerious