From 4c4540df84f7e71b26a696d246ecb6f655f0e1e7 Mon Sep 17 00:00:00 2001 From: Mark Fields Date: Sat, 16 May 2026 20:45:31 +0000 Subject: [PATCH 1/6] Add error logging and decoration when provideEntryPoint throws --- .../runtime/datastore/src/dataStoreRuntime.ts | 20 ++++++++++++++++++- packages/utils/telemetry-utils/src/error.ts | 3 +-- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/packages/runtime/datastore/src/dataStoreRuntime.ts b/packages/runtime/datastore/src/dataStoreRuntime.ts index da14c2438e5d..a0e85a0167a5 100644 --- a/packages/runtime/datastore/src/dataStoreRuntime.ts +++ b/packages/runtime/datastore/src/dataStoreRuntime.ts @@ -506,7 +506,25 @@ export class FluidDataStoreRuntime } this.entryPoint = new FluidObjectHandle( - new LazyPromise(async () => provideEntryPoint(this)), + new LazyPromise(async () => + provideEntryPoint(this).catch((error) => { + const errorWrapped = DataProcessingError.wrapIfUnrecognized( + error, + "entryPointInitialization", + ); + errorWrapped.addTelemetryProperties( + tagCodeArtifacts({ + fullPackageName: this.dataStoreContext.packagePath.join("/"), + fluidDataStoreId: this.id, + }), + ); + this.mc.logger.sendErrorEvent( + { eventName: "EntryPointInitializationFailure" }, + errorWrapped, + ); + throw errorWrapped; + }), + ), "", this.objectsRoutingContext, ); diff --git a/packages/utils/telemetry-utils/src/error.ts b/packages/utils/telemetry-utils/src/error.ts index 89b37bb01245..9009d0994f6e 100644 --- a/packages/utils/telemetry-utils/src/error.ts +++ b/packages/utils/telemetry-utils/src/error.ts @@ -295,8 +295,7 @@ export class DataProcessingError extends LoggingError implements IErrorBase, IFl messageLike?: MessageLike, ): IFluidErrorBase { return wrapDataProcessingErrorIfUnrecognized( - (errorMessage: string, props?: ITelemetryBaseProperties) => - new DataProcessingError(errorMessage, props), + (errorMessage: string) => new DataProcessingError(errorMessage), originalError, dataProcessingCodepath, messageLike, From c99c8120fb164ebb64ce0ab1ee778f16455d066b Mon Sep 17 00:00:00 2001 From: Mark Fields Date: Sat, 16 May 2026 20:45:53 +0000 Subject: [PATCH 2/6] Centralize datastore load telemetry props between entryPoint and realize --- .../container-runtime/src/dataStoreContext.ts | 8 ++----- .../runtime/datastore/src/dataStoreRuntime.ts | 6 ++--- .../runtime-utils/src/dataStoreHelpers.ts | 22 ++++++++++++++++++- packages/runtime/runtime-utils/src/index.ts | 1 + 4 files changed, 26 insertions(+), 11 deletions(-) diff --git a/packages/runtime/container-runtime/src/dataStoreContext.ts b/packages/runtime/container-runtime/src/dataStoreContext.ts index fc4de5a575b1..9ad68394a8d2 100644 --- a/packages/runtime/container-runtime/src/dataStoreContext.ts +++ b/packages/runtime/container-runtime/src/dataStoreContext.ts @@ -75,6 +75,7 @@ import { channelsTreeName } from "@fluidframework/runtime-definitions/internal"; import { addBlobToSummary, isSnapshotFetchRequiredForLoadingGroupId, + dataStoreLoadTelemetryProps, } from "@fluidframework/runtime-utils/internal"; import { DataProcessingError, @@ -587,12 +588,7 @@ export abstract class FluidDataStoreContext error, "realizeFluidDataStoreContext", ); - errorWrapped.addTelemetryProperties( - tagCodeArtifacts({ - fullPackageName: this.pkg?.join("/"), - fluidDataStoreId: this.id, - }), - ); + errorWrapped.addTelemetryProperties(dataStoreLoadTelemetryProps(this)); this.mc.logger.sendErrorEvent({ eventName: "RealizeError" }, errorWrapped); throw errorWrapped; }); diff --git a/packages/runtime/datastore/src/dataStoreRuntime.ts b/packages/runtime/datastore/src/dataStoreRuntime.ts index a0e85a0167a5..b101da88ea9a 100644 --- a/packages/runtime/datastore/src/dataStoreRuntime.ts +++ b/packages/runtime/datastore/src/dataStoreRuntime.ts @@ -78,6 +78,7 @@ import { exceptionToResponse, generateHandleContextPath, processAttachMessageGCData, + dataStoreLoadTelemetryProps, toFluidHandleInternal, unpackChildNodesUsedRoutes, toDeltaManagerErased, @@ -513,10 +514,7 @@ export class FluidDataStoreRuntime "entryPointInitialization", ); errorWrapped.addTelemetryProperties( - tagCodeArtifacts({ - fullPackageName: this.dataStoreContext.packagePath.join("/"), - fluidDataStoreId: this.id, - }), + dataStoreLoadTelemetryProps(this.dataStoreContext), ); this.mc.logger.sendErrorEvent( { eventName: "EntryPointInitializationFailure" }, diff --git a/packages/runtime/runtime-utils/src/dataStoreHelpers.ts b/packages/runtime/runtime-utils/src/dataStoreHelpers.ts index 4ee0dd56df20..b4a19041205c 100644 --- a/packages/runtime/runtime-utils/src/dataStoreHelpers.ts +++ b/packages/runtime/runtime-utils/src/dataStoreHelpers.ts @@ -12,8 +12,13 @@ import type { import type { ContainerRuntimeBaseAlpha, IContainerRuntimeBase, + IFluidDataStoreContext, } from "@fluidframework/runtime-definitions/internal"; -import { generateErrorWithStack } from "@fluidframework/telemetry-utils/internal"; +import { + generateErrorWithStack, + tagCodeArtifacts, + type ITelemetryPropertiesExt, +} from "@fluidframework/telemetry-utils/internal"; interface IResponseException extends Error { errorFromRequestFluidObject: true; @@ -140,6 +145,21 @@ export function createResponseError( }; } +/** + * Returns the canonical set of code-artifact-tagged telemetry properties identifying a data store. + * Use this anywhere a data store identity needs to appear in telemetry, so all such logs use + * consistent property names. + * @internal + */ +export function dataStoreLoadTelemetryProps( + context: IFluidDataStoreContext, +): ITelemetryPropertiesExt { + return tagCodeArtifacts({ + fullPackageName: context.packagePath.join("/"), + fluidDataStoreId: context.id, + }); +} + /** * Converts types to their alpha counterparts to expose alpha functionality. * @legacy @alpha diff --git a/packages/runtime/runtime-utils/src/index.ts b/packages/runtime/runtime-utils/src/index.ts index 8a89c2ad6191..289587c98aa3 100644 --- a/packages/runtime/runtime-utils/src/index.ts +++ b/packages/runtime/runtime-utils/src/index.ts @@ -10,6 +10,7 @@ export { exceptionToResponse, responseToException, asLegacyAlpha, + dataStoreLoadTelemetryProps, } from "./dataStoreHelpers.js"; export { compareFluidHandles, From 3091f0db908045c5e4638e3afe6209a680e6309a Mon Sep 17 00:00:00 2001 From: Mark Fields Date: Sat, 16 May 2026 21:11:35 +0000 Subject: [PATCH 3/6] unit tests --- .../src/test/dataStoreRuntime.spec.ts | 81 +++++++++++++++++++ 1 file changed, 81 insertions(+) diff --git a/packages/runtime/datastore/src/test/dataStoreRuntime.spec.ts b/packages/runtime/datastore/src/test/dataStoreRuntime.spec.ts index ed1acbec6a86..6f51fcf051a6 100644 --- a/packages/runtime/datastore/src/test/dataStoreRuntime.spec.ts +++ b/packages/runtime/datastore/src/test/dataStoreRuntime.spec.ts @@ -21,6 +21,11 @@ import type { ISequencedMessageEnvelope, MinimumVersionForCollab, } from "@fluidframework/runtime-definitions/internal"; +import { + isFluidError, + MockLogger, + TelemetryDataTag, +} from "@fluidframework/telemetry-utils/internal"; import { MockFluidDataStoreContext, validateAssertionError, @@ -68,6 +73,7 @@ describe("FluidDataStoreRuntime Tests", () => { // back-compat 0.38 - DataStoreRuntime looks in container runtime for certain properties that are unavailable // in the data store context. dataStoreContext.containerRuntime = {} as unknown as IContainerRuntimeBase; + dataStoreContext.packagePath = []; sharedObjectRegistry = { get(type: string) { return { @@ -226,6 +232,81 @@ describe("FluidDataStoreRuntime Tests", () => { "entryPoint was not initialized", ); }); + + describe("entryPoint initialization failure", () => { + it("entryPoint provider is not invoked until entryPoint is consumed", () => { + let invoked = false; + createRuntime(dataStoreContext, sharedObjectRegistry, async () => { + invoked = true; + return {}; + }); + assert.strictEqual( + invoked, + false, + "entryPoint provider should not run during construction", + ); + }); + + it("rejected entryPoint provider is wrapped and logged", async () => { + const mockLogger = new MockLogger(); + const contextWithMockLogger = new MockFluidDataStoreContext( + "testDataStoreId", + false, + mockLogger.toTelemetryLogger(), + ); + contextWithMockLogger.containerRuntime = {} as unknown as IContainerRuntimeBase; + contextWithMockLogger.packagePath = ["pkgA", "pkgB"]; + const dataStoreRuntime = createRuntime( + contextWithMockLogger, + sharedObjectRegistry, + async () => { + throw new Error("entryPoint failed"); + }, + ); + await assert.rejects( + async () => dataStoreRuntime.entryPoint.get(), + (error: IErrorBase) => { + assert.strictEqual( + error.errorType, + ContainerErrorTypes.dataProcessingError, + "thrown error should be a DataProcessingError", + ); + assert(isFluidError(error), "thrown error should be a Fluid error"); + const props = error.getTelemetryProperties(); + assert.deepStrictEqual( + props.fluidDataStoreId, + { value: "testDataStoreId", tag: TelemetryDataTag.CodeArtifact }, + "error should carry tagged fluidDataStoreId", + ); + assert.deepStrictEqual( + props.fullPackageName, + { value: "pkgA/pkgB", tag: TelemetryDataTag.CodeArtifact }, + "error should carry tagged fullPackageName", + ); + return true; + }, + ); + const failureEvent = mockLogger.events.find( + (event) => + typeof event.eventName === "string" && + event.eventName.endsWith("EntryPointInitializationFailure"), + ); + assert( + failureEvent !== undefined, + "EntryPointInitializationFailure event should have been logged", + ); + assert.deepStrictEqual( + failureEvent.fluidDataStoreId, + { value: "testDataStoreId", tag: TelemetryDataTag.CodeArtifact }, + "event should include tagged fluidDataStoreId", + ); + assert.deepStrictEqual( + failureEvent.fullPackageName, + { value: "pkgA/pkgB", tag: TelemetryDataTag.CodeArtifact }, + "event should include tagged fullPackageName", + ); + }); + }); }); describe("FluidDataStoreRuntime.isDirty tracking", () => { From 6adb42d055c453d55df3e2e4d2f8bb4dd20e22c6 Mon Sep 17 00:00:00 2001 From: Mark Fields Date: Sat, 16 May 2026 22:04:00 +0000 Subject: [PATCH 4/6] Handle not-yet-undefined pkg --- .../runtime/runtime-utils/src/dataStoreHelpers.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/packages/runtime/runtime-utils/src/dataStoreHelpers.ts b/packages/runtime/runtime-utils/src/dataStoreHelpers.ts index b4a19041205c..5cce4c33a40c 100644 --- a/packages/runtime/runtime-utils/src/dataStoreHelpers.ts +++ b/packages/runtime/runtime-utils/src/dataStoreHelpers.ts @@ -154,8 +154,17 @@ export function createResponseError( export function dataStoreLoadTelemetryProps( context: IFluidDataStoreContext, ): ITelemetryPropertiesExt { + // `packagePath` is typed as always defined, but its implementation asserts on the package being + // set — and during early load-failure paths (e.g. realize() rejecting before pkg is read) it + // can throw. Swallow that so error decoration never replaces the underlying error. + let fullPackageName: string | undefined; + try { + fullPackageName = context.packagePath.join("/"); + } catch { + // `packagePath` is unset during early failures; leave fullPackageName undefined. + } return tagCodeArtifacts({ - fullPackageName: context.packagePath.join("/"), + fullPackageName, fluidDataStoreId: context.id, }); } From 1a05491ba18a17ad9dcb3b9b66651031962ef666 Mon Sep 17 00:00:00 2001 From: Mark Fields Date: Sun, 17 May 2026 02:30:40 +0000 Subject: [PATCH 5/6] Another test fix --- .../src/test/summarization/summaries.spec.ts | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/packages/test/test-end-to-end-tests/src/test/summarization/summaries.spec.ts b/packages/test/test-end-to-end-tests/src/test/summarization/summaries.spec.ts index 41dc21b6b1c7..0a8d7d5a7862 100644 --- a/packages/test/test-end-to-end-tests/src/test/summarization/summaries.spec.ts +++ b/packages/test/test-end-to-end-tests/src/test/summarization/summaries.spec.ts @@ -296,7 +296,16 @@ describeCompat("Summaries", "NoCompat", (getTestObjectProvider, apis) => { ); }); - it("full initialization of data object should not happen by default", async () => { + itExpects( + "full initialization of data object should not happen by default", + [ + { + eventName: + "fluid:telemetry:FluidDataStoreRuntime:EntryPointInitializationFailure", + error: "Non interactive/summarizer client's data object should not be initialized", + }, + ], + async () => { const dataStoreFactory1 = new DataObjectFactory({ type: "@fluid-example/test-dataStore1", ctor: TestDataObject1, @@ -349,7 +358,8 @@ describeCompat("Summaries", "NoCompat", (getTestObjectProvider, apis) => { container2.getEntryPoint(), "Initial creation of container and data store should succeed.", ); - }); + }, + ); /** * This test validates that the first summary for a container by the first summarizer client does not violate From 4fbb68a741d810ed518628432736ad93bf565cc9 Mon Sep 17 00:00:00 2001 From: Mark Fields Date: Sun, 17 May 2026 02:36:08 +0000 Subject: [PATCH 6/6] formatting --- .../src/test/summarization/summaries.spec.ts | 100 +++++++++--------- 1 file changed, 50 insertions(+), 50 deletions(-) diff --git a/packages/test/test-end-to-end-tests/src/test/summarization/summaries.spec.ts b/packages/test/test-end-to-end-tests/src/test/summarization/summaries.spec.ts index 0a8d7d5a7862..ee67befe8ed6 100644 --- a/packages/test/test-end-to-end-tests/src/test/summarization/summaries.spec.ts +++ b/packages/test/test-end-to-end-tests/src/test/summarization/summaries.spec.ts @@ -300,64 +300,64 @@ describeCompat("Summaries", "NoCompat", (getTestObjectProvider, apis) => { "full initialization of data object should not happen by default", [ { - eventName: - "fluid:telemetry:FluidDataStoreRuntime:EntryPointInitializationFailure", + eventName: "fluid:telemetry:FluidDataStoreRuntime:EntryPointInitializationFailure", error: "Non interactive/summarizer client's data object should not be initialized", }, ], async () => { - const dataStoreFactory1 = new DataObjectFactory({ - type: "@fluid-example/test-dataStore1", - ctor: TestDataObject1, - }); - const registryStoreEntries = new Map>([ - [dataStoreFactory1.type, Promise.resolve(dataStoreFactory1)], - ]); - const runtimeFactory = new ContainerRuntimeFactoryWithDefaultDataStore({ - defaultFactory: dataStoreFactory1, - registryEntries: registryStoreEntries, - }); + const dataStoreFactory1 = new DataObjectFactory({ + type: "@fluid-example/test-dataStore1", + ctor: TestDataObject1, + }); + const registryStoreEntries = new Map>([ + [dataStoreFactory1.type, Promise.resolve(dataStoreFactory1)], + ]); + const runtimeFactory = new ContainerRuntimeFactoryWithDefaultDataStore({ + defaultFactory: dataStoreFactory1, + registryEntries: registryStoreEntries, + }); - // Create a container for the first client. - const container1 = await provider.createContainer(runtimeFactory); - await assert.doesNotReject( - container1.getEntryPoint(), - "Initial creation of container and data store should succeed.", - ); + // Create a container for the first client. + const container1 = await provider.createContainer(runtimeFactory); + await assert.doesNotReject( + container1.getEntryPoint(), + "Initial creation of container and data store should succeed.", + ); - // Create a summarizer for the container and do a summary shouldn't throw. - const createSummarizerResult = await createSummarizerFromFactory( - provider, - container1, - dataStoreFactory1, - undefined, - ContainerRuntimeFactoryWithDefaultDataStore, - registryStoreEntries, - ); - await assert.doesNotReject( - summarizeNow(createSummarizerResult.summarizer, "test"), - "Summarizing should not throw", - ); + // Create a summarizer for the container and do a summary shouldn't throw. + const createSummarizerResult = await createSummarizerFromFactory( + provider, + container1, + dataStoreFactory1, + undefined, + ContainerRuntimeFactoryWithDefaultDataStore, + registryStoreEntries, + ); + await assert.doesNotReject( + summarizeNow(createSummarizerResult.summarizer, "test"), + "Summarizing should not throw", + ); - // In summarizer, load the data store should fail. - await assert.rejects( - async () => { - const runtime = (createSummarizerResult.summarizer as any).runtime as ContainerRuntime; - const dsEntryPoint = await runtime.getAliasedDataStoreEntryPoint("default"); - await dsEntryPoint?.get(); - }, - (e: Error) => - e.message === - "Non interactive/summarizer client's data object should not be initialized", - "Loading data store in summarizer did not throw as it should, or threw an unexpected error.", - ); + // In summarizer, load the data store should fail. + await assert.rejects( + async () => { + const runtime = (createSummarizerResult.summarizer as any) + .runtime as ContainerRuntime; + const dsEntryPoint = await runtime.getAliasedDataStoreEntryPoint("default"); + await dsEntryPoint?.get(); + }, + (e: Error) => + e.message === + "Non interactive/summarizer client's data object should not be initialized", + "Loading data store in summarizer did not throw as it should, or threw an unexpected error.", + ); - // Load second container, load the data store will also call initializingFromExisting and succeed. - const container2 = await provider.loadContainer(runtimeFactory); - await assert.doesNotReject( - container2.getEntryPoint(), - "Initial creation of container and data store should succeed.", - ); + // Load second container, load the data store will also call initializingFromExisting and succeed. + const container2 = await provider.loadContainer(runtimeFactory); + await assert.doesNotReject( + container2.getEntryPoint(), + "Initial creation of container and data store should succeed.", + ); }, );