Skip to content

feat(tree): emit telemetry when healing unresolvable identifiers on decode - #27756

Open
CraigMacomber wants to merge 5 commits into
microsoft:mainfrom
CraigMacomber:tree/heal-identifier-decode-telemetry
Open

feat(tree): emit telemetry when healing unresolvable identifiers on decode#27756
CraigMacomber wants to merge 5 commits into
microsoft:mainfrom
CraigMacomber:tree/heal-identifier-decode-telemetry

Conversation

@CraigMacomber

@CraigMacomber CraigMacomber commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Description

When SharedTreeOptionsBeta.healUnresolvableIdentifiersOnDecode is enabled, SharedTree now records a HealUnresolvableIdentifierOnDecode telemetry event (at LogLevel.info) each time a non-final identifier is healed while loading a summary. This lets applications relying on the healing workaround observe which documents actually required healing.

This is a follow-up to the identifier-healing work: the heal and throw paths only exist because of a prior bug where non-finalized identifiers could be written into summaries. Instrumenting the recovery path makes those occurrences visible in the wild. The error/throw path is intentionally left un-instrumented — the thrown exception already surfaces via the application's own error telemetry and must not be silently swallowed.

Changes:

  • Add an optional logger to the internal IdentifierHealingConfig; forceDecodeEncodedIdWithoutSession sends the event directly via logger?.send(..., LogLevel.essential).
  • Thread the shared object's logger into the healing config in SharedTreeKernel and SharedTreeCore.
  • Document the telemetry event and level on healUnresolvableIdentifiersOnDecode.
  • Add unit tests and a changeset.

Because it changes observable behavior for consumers who opted into the healing feature (they now get telemetry), this ships with a changeset.

Reviewer Guidance

The review process is outlined on this wiki page.

  • The logger lives on IdentifierHealingConfig (recovery path only) rather than being threaded independently; the throw path is deliberately not instrumented. Confirm that split is acceptable.

…ecode

When healUnresolvableIdentifiersOnDecode is enabled, SharedTree now records a HealUnresolvableIdentifierOnDecode telemetry event (at LogLevel.info) each time a non-final identifier is healed during summary load, so affected documents can be observed. Adds an optional logger to IdentifierHealingConfig, threads the shared object's logger through, documents the behavior, and adds a changeset.
@github-actions github-actions Bot added area: tools area: dds Issues related to distributed data structures area: repo Repo related work area: website area: dds: tree changeset-present base: main PRs targeted against main branch labels Jul 24, 2026
@CraigMacomber
CraigMacomber marked this pull request as ready for review July 24, 2026 18:47
@CraigMacomber
CraigMacomber requested review from a team as code owners July 24, 2026 18:47
Copilot AI review requested due to automatic review settings July 24, 2026 18:47
@github-actions

github-actions Bot commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Hi! Thank you for opening this PR. Want me to review it?

Based on the diff (76 lines, 5 files), I've queued these reviewers:

  • Correctness — logic errors, race conditions, lifecycle issues
  • Security — vulnerabilities, secret exposure, injection
  • API Compatibility — breaking changes, release tags, type design
  • Performance — algorithmic regressions, memory leaks
  • Testing — coverage gaps, hollow tests

How this works

  • Adjust the reviewer set by ticking/unticking boxes above. Reviewer toggles alone don't trigger anything.

  • Tick Start review below to dispatch the review fleet.

  • After review finishes, tick Start review again to request another run — it auto-resets after each dispatch.

  • This comment updates as new commits land; your reviewer selections are preserved.

  • Start review

Comment thread packages/dds/tree/src/util/compressedIds.ts

Copilot AI 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.

Copilot reviewed 5 out of 5 changed files in this pull request and generated no comments.

@dannimad dannimad 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.

Wait for #27764 to be merged first

Comment thread packages/dds/tree/src/util/compressedIds.ts Outdated
Comment thread packages/dds/tree/src/test/util/compressedIds.spec.ts Outdated
@Josmithr
Josmithr requested review from a team and markfields July 28, 2026 17:29
Co-authored-by: Joshua Smithrud <54606601+Josmithr@users.noreply.github.com>
category: "generic",
eventName: "HealUnresolvableIdentifierOnDecode",
},
LogLevel.info,

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.

I do think this should be essential. Rationale -the other side of the if (healing !== undefined) check throws an error which we will for sure log at essential level (probably with multiple events as it propagates).

So we should match both branches to be able to definitively monitor the fix. I'm not worried about the overall volume since there's natural attrition due to not using the document or subsequent summaries persisting the fixed ID.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done.

const healed = uuidV5(`${healing.sharedObjectId}|${id}`, healingNamespace);
healing.logger?.send(
{
category: "generic",

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.

If you switch the type of logger to be ITelemetryLoggerExt and use sendEvent or sendTelemetryEvent or whatever then you won't need this category ("generic" is default). That's the interface we typically use for intrumenting. (The base logger is what is exposed across seams for passing logs around)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

There are reasons not to do this, mostly related to potential future refactors and shared-tree internals which can accept loggers directly from users which might not implement ITelemetryLoggerExt.
Specifically if this code becomes reachable from IndependentViews which don't have a main fluid logger, we could run into issues.

);
});

it("does not log when the ID is resolvable", () => {

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.

Want to add a test case for not logging when it throws (not resolvable and not healed)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

That case is only possible when forceDecodeEncodedIdWithoutSession is passed no IdentifierHealingConfig, and since the logger is part of IdentifierHealingConfig, that would involve testing that an API which is not passed the longer does not log anything.

I don't think thats helpful for two reasons:

  1. If would actually be ok if we logged telemetry in the error case.
  2. Its impossible that it would log anything as it has no access to the logger.

We could add a higher level integration case for that case, but that would amount to testing that loading a shared tree with an invalid summary throws a data corruption error (which would be logged), and I hope we have testing for that somewhere else. We do have testing in this file that an unrecovered ID does throw.

@dannimad

dannimad commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

wait for #27824 to merge

@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Bundle size comparison

Base commit: 105abcb748df4491dafe568c9930b0107569110e
Head commit: 03ac2f871556ff2bedc2f37533903ea56ecfac9c

Notable changes

  • 🔴 directory.js: parsed 66616 → 67117 (+501), gzip 18532 → 18866 (+334)
  • 748.js: removed (was parsed 58793, gzip 17827)
  • 🔴 map.js: parsed 46709 → 47212 (+503), gzip 14310 → 14462 (+152)
  • 985.js: removed (was parsed 44491, gzip 13726)
  • 578.js: added (parsed 58686, gzip 17657)
  • 252.js: added (parsed 44371, gzip 13732)
Per-bundle deltas

@fluid-example/bundle-size-tests

  • azureClient.js: parsed 620317 → 620407 (+90), gzip 165071 → 165524 (+453)
  • odspClient.js: parsed 593043 → 592695 (-348), gzip 159171 → 158651 (-520)
  • aqueduct.js: parsed 526806 → 527005 (+199), gzip 140956 → 141062 (+106)
  • fluidFramework.js: parsed 398444 → 398369 (-75), gzip 113068 → 113117 (+49)
  • sharedTree.js: parsed 387831 → 387766 (-65), gzip 110466 → 110541 (+75)
  • containerRuntime.js: parsed 305156 → 305105 (-51), gzip 83445 → 83558 (+113)
  • sharedString.js: parsed 175984 → 176464 (+480), gzip 49445 → 49815 (+370)
  • experimentalSharedTree.js: parsed 160798 → 160677 (-121), gzip 45804 → 46276 (+472)
  • matrix.js: parsed 159845 → 160309 (+464), gzip 45411 → 45807 (+396)
  • loader.js: parsed 145256 → 145473 (+217), gzip 39063 → 39235 (+172)
  • odspDriver.js: parsed 104329 → 103927 (-402), gzip 32625 → 32411 (-214)
  • 🔴 directory.js: parsed 66616 → 67117 (+501), gzip 18532 → 18866 (+334)
  • 748.js: removed (was parsed 58793, gzip 17827)
  • 🔴 map.js: parsed 46709 → 47212 (+503), gzip 14310 → 14462 (+152)
  • odspPrefetchSnapshot.js: parsed 45642 → 45649 (+7), gzip 15277 → 15249 (-28)
  • 985.js: removed (was parsed 44491, gzip 13726)
  • summarizerDelayLoadedModule.js: parsed 30749 → 30717 (-32), gzip 7753 → 7716 (-37)
  • socketModule.js: parsed 26476 → 26476 (0), gzip 7887 → 7903 (+16)
  • createNewModule.js: parsed 12480 → 12454 (-26), gzip 4786 → 4797 (+11)
  • summaryModule.js: parsed 3797 → 3789 (-8), gzip 1860 → 1857 (-3)
  • connectionState.js: parsed 724 → 909 (+185), gzip 429 → 500 (+71)
  • sharedTreeAttributes.js: parsed 666 → 854 (+188), gzip 433 → 508 (+75)
  • debugAssert.js: parsed 429 → 429 (0), gzip 299 → 299 (0)
  • FluidFramework-HashFallback.js: parsed 422 → 419 (-3), gzip 316 → 313 (-3)
  • 578.js: added (parsed 58686, gzip 17657)
  • 252.js: added (parsed 44371, gzip 13732)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: dds: tree area: dds Issues related to distributed data structures area: repo Repo related work area: tools area: website base: main PRs targeted against main branch changeset-present

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants