Skip to content

Conversation

@Takeno
Copy link
Contributor

@Takeno Takeno commented Dec 14, 2025

This PR introduces comprehensive observability support for Jazz using the OpenTelemetry protocol. It instruments the SubscriptionScope with metrics and tracing spans, allowing developers to integrate Jazz's internal performance data with their existing observability stack.

Metrics are based on performance.marks and performance.measure, and they are visible in the DevTools.

immagine

A new performance page has been added to the Inspector, displaying load times with detailed breakdown (storage, peer, parsing). It is available only for in-app inspector, as it uses the same context.

immagine

Note

Adds end-to-end observability for subscription performance and exposes it in-app.

  • Instrumentation (OTel): Instrumented SubscriptionScope with jazz.subscription.active (UpDownCounter) and jazz.subscription.first_load (Histogram), plus tracing spans (jazz.subscription.*). New unstable_setOpenTelemetryInstrumentationEnabled toggle. CoJSON adds performance marks for storage/peer loads and exports perf.performanceMarks.
  • Inspector UI: New Perf tab shows subscription load times (storage/peer/memory), active subscriptions, and transport metrics; includes jazzMetricReader and auto meter setup in JazzInspector.
  • Docs & examples: New Observability docs and code snippets; Inspector docs updated for Perf tab; homepage adds OpenTelemetry deps and nav entry; example app embeds JazzInspector within provider.
  • Tests: Added OTel metric reader test utilities and tests for active subscriptions and first-load duration.

Written by Cursor Bugbot for commit a11bedb. This will update automatically on new commits. Configure here.

@vercel
Copy link

vercel bot commented Dec 14, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Review Updated (UTC)
clerk-demo Ready Ready Preview, Comment Dec 29, 2025 11:06am
design-system Ready Ready Preview, Comment Dec 29, 2025 11:06am
file-upload-demo Ready Ready Preview, Comment Dec 29, 2025 11:06am
form-demo Ready Ready Preview, Comment Dec 29, 2025 11:06am
gcmp-homepage Ready Ready Preview, Comment Dec 29, 2025 11:06am
image-upload-demo Ready Ready Preview, Comment Dec 29, 2025 11:06am
jazz-chat Ready Ready Preview, Comment Dec 29, 2025 11:06am
jazz-chat-1 Ready Ready Preview, Comment Dec 29, 2025 11:06am
jazz-chat-2 Ready Ready Preview, Comment Dec 29, 2025 11:06am
jazz-filestream Ready Ready Preview, Comment Dec 29, 2025 11:06am
jazz-image-upload Ready Ready Preview, Comment Dec 29, 2025 11:06am
jazz-inspector Ready Ready Preview, Comment Dec 29, 2025 11:06am
jazz-multi-cursors Ready Ready Preview, Comment Dec 29, 2025 11:06am
jazz-nextjs Ready Ready Preview, Comment Dec 29, 2025 11:06am
jazz-organization Ready Ready Preview, Comment Dec 29, 2025 11:06am
jazz-paper-scissors Ready Ready Preview, Comment Dec 29, 2025 11:06am
jazz-richtext Ready Ready Preview, Comment Dec 29, 2025 11:06am
jazz-todo Ready Ready Preview, Comment Dec 29, 2025 11:06am
jazz-vector-search Ready Ready Preview, Comment Dec 29, 2025 11:06am
jazz-version-history Ready Ready Preview, Comment Dec 29, 2025 11:06am
music-demo Ready Ready Preview, Comment Dec 29, 2025 11:06am
passkey-demo Ready Ready Preview, Comment Dec 29, 2025 11:06am
passphrase-auth-demo Ready Ready Preview, Comment Dec 29, 2025 11:06am
quint-ui Ready Ready Preview, Comment Dec 29, 2025 11:06am
1 Skipped Deployment
Project Deployment Review Updated (UTC)
jazz-homepage Ignored Ignored Preview Dec 29, 2025 11:06am

@blacksmith-sh

This comment has been minimized.

@Takeno Takeno marked this pull request as ready for review December 20, 2025 17:14
@Takeno Takeno requested review from a team and aeplay December 20, 2025 17:14
Copy link
Collaborator

@gdorsi gdorsi left a comment

Choose a reason for hiding this comment

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

Love the feature!

I think we should start way more slowly, and minimize the information exposed as much as possible.

Other than the performance impact concerns I think we should carefully expose only the extremely valuable informations, otherwise we would end up like the React's performance marks that I myself never got to understand.

Advanced users can use the performance profiler that gives detailed info anyway.

Also, React Native is failing with failure: com.facebook.react.common.JavascriptException: TypeError: undefined is not a function

loadTime: number;
loadFromStorage?: number;
loadFromPeer?: number;
transactionParsing?: number;
Copy link
Collaborator

Choose a reason for hiding this comment

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

I like the idea of measuring storage vs network load time, but instead of exposing it this way I think it would be easier to understand if we give people a type flag that's storage if the value was found on storage and network otherwise.

A single loadTime number is more than enough to catch slow queries, the rest is for advanced investigations, but I think that even in that case it would be hard to gain good insights given their very small granularity.

When things are going to be slow (I expect on thousands of CoValues) we need to show only the meaningful data, and avoid polluting the devtools with too many marks.

@Takeno
Copy link
Contributor Author

Takeno commented Dec 23, 2025

PR updated with latest feedback:

  • New OTel metrics and spans are disabled by default, and must be activated with unstable_setOpenTelemetryInstrumentationEnabled
  • Removed marks from decrypt transactions
  • Added a simple approach to clean up performance marks once read, to avoid useless pollution
  • Inspector now reports only the peer from the CoValue has been loaded (storage or network)
immagine
  • Implemented a clear button to filter out covalue load times at runtime
  • Updated the documentation
  • Auto-register the instrumentation when importing the inspector, to avoid manual configurations

@blacksmith-sh

This comment has been minimized.

Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

This PR is being reviewed by Cursor Bugbot

Details

Your team is on the Bugbot Free tier. On this plan, Bugbot will review limited PRs each billing cycle for each member of your team.

To receive Bugbot reviews on all of your PRs, visit the Cursor dashboard to activate Pro and start your 14-day free trial.

| typeof CoValueLoadingState.UNAVAILABLE
| undefined;

trackPerformanceMark("subscriptionLoadStart", id);
Copy link

Choose a reason for hiding this comment

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

Performance marks leak when subscription destroyed early

The subscriptionLoadStart performance mark is created unconditionally in the constructor, but cleanup only occurs in extractStartEndMarks when both start and end marks are synchronized. If a subscription is destroyed before trackFirstLoad() is called (e.g., unmounted while still loading), the start mark is never cleaned up. The destroy() method doesn't clear these orphaned performance marks, causing a minor memory leak in the performance timeline.

Additional Locations (1)

Fix in Cursor Fix in Web

id: this.id,
source_id: this.sourceId,
resolve: JSON.stringify(this.resolve),
});
Copy link

Choose a reason for hiding this comment

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

JSON.stringify runs unconditionally despite opt-in documentation

The activeSubCounter.add() calls include JSON.stringify(this.resolve) which executes unconditionally on every subscription creation and destruction, even when OpenTelemetry instrumentation is disabled. The PR discussion notes that performance.mark is too slow for performance-critical code paths, yet JSON.stringify on potentially complex nested resolve objects is more expensive. The span creation at line 143 is correctly guarded by isOpenTelemetryInstrumentationEnabled(), but the counter operations are not, contradicting the documentation stating metrics are "opt-in and disabled by default for performance reasons."

Additional Locations (1)

Fix in Cursor Fix in Web

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This OTel counter is also used to track the number of active subscriptions in the inspector. We need an additional check if the inspector is installed or not.

@Takeno
Copy link
Contributor Author

Takeno commented Dec 29, 2025

PR Updated:

  • Simplified inspector's UI: now only top-level subscriptions are reported
  • Reduced the noise in the devTools' profiling, removing used marks and showing only top-level subscription's times
  • Tracked also coValues without subscribers
  • Introduced a console.error when Inspector is rendered twice.
immagine


// Assuming they are all sync, pick the last endMark entry position
const startMarkEntry = startMarks.at(startMarks.length - 1);
const endMarkEntry = endMarks.at(-1);
Copy link

Choose a reason for hiding this comment

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

Performance marks mismatch for concurrent subscriptions

The extractStartEndMarks function uses startMarks.at(startMarks.length - 1) and endMarks.at(-1) to get the last entries from each array. When multiple SubscriptionScope instances exist for the same coId concurrently, this causes mismatched start/end mark pairing. For example, if subscription A and B both start for the same coId (adding two start marks), and A completes first (adding one end mark), the measurement pairs B's start mark with A's end mark, producing an incorrect (shorter) duration. Each subscription should track its own unique marks rather than relying on global mark arrays keyed only by coId.

Fix in Cursor Fix in Web

export function recordMetrics() {
const globalMeterHasBeenSet = !metrics
.getMeterProvider()
.constructor.name.startsWith("Noop");
Copy link

Choose a reason for hiding this comment

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

Constructor name check fragile under minification

The recordMetrics function checks whether a global meter provider is configured by testing if constructor.name.startsWith("Noop"). In production builds with minification enabled, class names can be mangled (e.g., NoopMeterProvider becomes "n" or "t"). When minified, the check would incorrectly return true for globalMeterHasBeenSet (since a minified name like "n" doesn't start with "Noop"), causing the function to exit early without setting up the Jazz meter provider. This would silently break metrics collection in minified production builds.

Fix in Cursor Fix in Web

Sign up for free to 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.

4 participants