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 da14c2438e5d..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, @@ -506,7 +507,22 @@ 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( + dataStoreLoadTelemetryProps(this.dataStoreContext), + ); + this.mc.logger.sendErrorEvent( + { eventName: "EntryPointInitializationFailure" }, + errorWrapped, + ); + throw errorWrapped; + }), + ), "", this.objectsRoutingContext, ); 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", () => { diff --git a/packages/runtime/runtime-utils/src/dataStoreHelpers.ts b/packages/runtime/runtime-utils/src/dataStoreHelpers.ts index 4ee0dd56df20..5cce4c33a40c 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,30 @@ 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 { + // `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, + 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, 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..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 @@ -296,60 +296,70 @@ describeCompat("Summaries", "NoCompat", (getTestObjectProvider, apis) => { ); }); - it("full initialization of data object should not happen by default", 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, - }); + 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, + }); + 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.", + ); + }, + ); /** * This test validates that the first summary for a container by the first summarizer client does not violate 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,