@fluidframework/telemetry-utils 2.20.0 → 2.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"mockLogger.js","sourceRoot":"","sources":["../src/mockLogger.ts"],"names":[],"mappings":"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","sourcesContent":["/*!\n * Copyright (c) Microsoft Corporation and contributors. All rights reserved.\n * Licensed under the MIT License.\n */\n\nimport {\n\ttype ITelemetryBaseEvent,\n\ttype ITelemetryBaseLogger,\n\tLogLevel,\n} from \"@fluidframework/core-interfaces\";\nimport { assert } from \"@fluidframework/core-utils/internal\";\n\nimport { createChildLogger } from \"./logger.js\";\nimport type {\n\tITelemetryEventExt,\n\tITelemetryLoggerExt,\n\tITelemetryPropertiesExt,\n} from \"./telemetryTypes.js\";\n\n/**\n * Mock {@link @fluidframework/core-interfaces#ITelemetryBaseLogger} implementation.\n *\n * Records events sent to it, and then can walk back over those events, searching for a set of expected events to\n * match against the logged events.\n *\n * @internal\n */\nexport class MockLogger implements ITelemetryBaseLogger {\n\t/**\n\t * Gets an immutable copy of the events logged thus far.\n\t */\n\tpublic get events(): readonly ITelemetryBaseEvent[] {\n\t\treturn [...this._events];\n\t}\n\n\tprivate _events: ITelemetryBaseEvent[] = [];\n\n\t/**\n\t * {@inheritDoc @fluidframework/core-interfaces#ITelemetryBaseLogger.minLogLevel}\n\t */\n\tpublic readonly minLogLevel: LogLevel;\n\n\tpublic constructor(minLogLevel?: LogLevel) {\n\t\tthis.minLogLevel = minLogLevel ?? LogLevel.default;\n\t}\n\n\t/**\n\t * Clears the events logged thus far.\n\t */\n\tpublic clear(): void {\n\t\tthis._events = [];\n\t}\n\n\tpublic toTelemetryLogger(): ITelemetryLoggerExt {\n\t\treturn createChildLogger({ logger: this });\n\t}\n\n\t/**\n\t * {@inheritDoc @fluidframework/core-interfaces#ITelemetryBaseLogger.send}\n\t */\n\tpublic send(event: ITelemetryBaseEvent, logLevel?: LogLevel): void {\n\t\tif ((logLevel ?? LogLevel.default) >= this.minLogLevel) {\n\t\t\tthis._events.push(event);\n\t\t}\n\t}\n\n\t/**\n\t * Search events logged since the last time matchEvents was called, looking for the given expected\n\t * events in order.\n\t * @param expectedEvents - events in order that are expected to appear in the recorded log.\n\t * @param inlineDetailsProp - true if the \"details\" property in the actual event should be extracted and inlined.\n\t * These event objects may be subsets of the logged events.\n\t * Note: category is omitted from the type because it's usually uninteresting and tedious to type.\n\t * @param clearEventsAfterCheck - Whether or not to clear the logger's {@link MockLogger.events} after performing the check.\n\t * Default: true.\n\t */\n\tpublic matchEvents(\n\t\texpectedEvents: Omit<ITelemetryBaseEvent, \"category\">[],\n\t\tinlineDetailsProp: boolean = false,\n\t\tclearEventsAfterCheck: boolean = true,\n\t): boolean {\n\t\tconst matchedExpectedEventCount = this.getMatchedEventsCount(\n\t\t\texpectedEvents,\n\t\t\tinlineDetailsProp,\n\t\t\tclearEventsAfterCheck,\n\t\t);\n\t\t// How many expected events were left over? Hopefully none.\n\t\tconst unmatchedExpectedEventCount = expectedEvents.length - matchedExpectedEventCount;\n\t\treturn unmatchedExpectedEventCount === 0;\n\t}\n\n\t/**\n\t * Asserts {@link MockLogger.matchEvents} is `true` for the given events.\n\t * @param expectedEvents - The events expected to appear.\n\t * @param message - Optional error message to include in the thrown error, if the condition is not satisfied.\n\t * @param inlineDetailsProp - true if the \"details\" property in the actual event should be extracted and inlined.\n\t * These event objects may be subsets of the logged events.\n\t * Note: category is omitted from the type because it's usually uninteresting and tedious to type.\n\t * @param clearEventsAfterCheck - Whether or not to clear the logger's {@link MockLogger.events} after performing the check.\n\t * Default: true.\n\t * @throws An error containing the actual/expected event data if the condition is not satisfied.\n\t */\n\tpublic assertMatch(\n\t\texpectedEvents: Omit<ITelemetryBaseEvent, \"category\">[],\n\t\tmessage?: string,\n\t\tinlineDetailsProp: boolean = false,\n\t\tclearEventsAfterCheck: boolean = true,\n\t): void {\n\t\t// Use copy to ensure events aren't cleared out from under us before we (potentially) throw\n\t\tconst actualEvents = this.events;\n\n\t\tif (!this.matchEvents(expectedEvents, inlineDetailsProp, clearEventsAfterCheck)) {\n\t\t\tthrow new Error(`${message ?? \"Logs don't match\"}\nexpected:\n${JSON.stringify(expectedEvents)}\n\nactual:\n${JSON.stringify(actualEvents)}`);\n\t\t}\n\t}\n\n\t/**\n\t * Search events logged since the last time matchEvents was called, looking for any of the given\n\t * expected events.\n\t * @param expectedEvents - events that are expected to appear in the recorded log.\n\t * @param inlineDetailsProp - true if the \"details\" property in the actual event should be extracted and inlined.\n\t * These event objects may be subsets of the logged events.\n\t * Note: category is omitted from the type because it's usually uninteresting and tedious to type.\n\t * @returns if any of the expected events is found.\n\t */\n\tpublic matchAnyEvent(\n\t\texpectedEvents: Omit<ITelemetryBaseEvent, \"category\">[],\n\t\tinlineDetailsProp: boolean = false,\n\t\tclearEventsAfterCheck: boolean = true,\n\t): boolean {\n\t\tconst matchedExpectedEventCount = this.getMatchedEventsCount(\n\t\t\texpectedEvents,\n\t\t\tinlineDetailsProp,\n\t\t\tclearEventsAfterCheck,\n\t\t);\n\t\treturn matchedExpectedEventCount > 0;\n\t}\n\n\t/**\n\t * Asserts {@link MockLogger.matchAnyEvent} is `true` for the given events.\n\t * @param expectedEvents - The events expected to appear.\n\t * @param message - Optional error message to include in the thrown error, if the condition is not satisfied.\n\t * @param inlineDetailsProp - true if the \"details\" property in the actual event should be extracted and inlined.\n\t * These event objects may be subsets of the logged events.\n\t * Note: category is omitted from the type because it's usually uninteresting and tedious to type.\n\t * @param clearEventsAfterCheck - Whether or not to clear the logger's {@link MockLogger.events} after performing the check.\n\t * Default: true.\n\t * @throws An error containing the actual/expected event data if the condition is not satisfied.\n\t */\n\tpublic assertMatchAny(\n\t\texpectedEvents: Omit<ITelemetryBaseEvent, \"category\">[],\n\t\tmessage?: string,\n\t\tinlineDetailsProp: boolean = false,\n\t\tclearEventsAfterCheck: boolean = true,\n\t): void {\n\t\t// Use copy to ensure events aren't cleared out from under us before we (potentially) throw\n\t\tconst actualEvents = this.events;\n\n\t\tif (!this.matchAnyEvent(expectedEvents, inlineDetailsProp, clearEventsAfterCheck)) {\n\t\t\tthrow new Error(`${message ?? \"Logs don't match\"}\nexpected:\n${JSON.stringify(expectedEvents)}\n\nactual:\n${JSON.stringify(actualEvents)}`);\n\t\t}\n\t}\n\n\t/**\n\t * Search events logged since the last time matchEvents was called, looking only for the given expected\n\t * events in order.\n\t * @param expectedEvents - events in order that are expected to be the only events in the recorded log.\n\t * @param inlineDetailsProp - true if the \"details\" property in the actual event should be extracted and inlined.\n\t * These event objects may be subsets of the logged events.\n\t * Note: category is omitted from the type because it's usually uninteresting and tedious to type.\n\t * @param clearEventsAfterCheck - Whether or not to clear the logger's {@link MockLogger.events} after performing the check.\n\t * Default: true.\n\t */\n\tpublic matchEventStrict(\n\t\texpectedEvents: Omit<ITelemetryBaseEvent, \"category\">[],\n\t\tinlineDetailsProp: boolean = false,\n\t\tclearEventsAfterCheck: boolean = true,\n\t): boolean {\n\t\tif (expectedEvents.length !== this._events.length) {\n\t\t\tif (clearEventsAfterCheck) {\n\t\t\t\tthis.clear();\n\t\t\t}\n\t\t\treturn false;\n\t\t}\n\n\t\t// `events` will be cleared by the below check if requested.\n\t\treturn this.matchEvents(expectedEvents, inlineDetailsProp, clearEventsAfterCheck);\n\t}\n\n\t/**\n\t * Asserts {@link MockLogger.matchEvents} is `true` for the given events.\n\t * @param expectedEvents - The events expected to appear.\n\t * @param message - Optional error message to include in the thrown error, if the condition is not satisfied.\n\t * @param inlineDetailsProp - true if the \"details\" property in the actual event should be extracted and inlined.\n\t * These event objects may be subsets of the logged events.\n\t * Note: category is omitted from the type because it's usually uninteresting and tedious to type.\n\t * @param clearEventsAfterCheck - Whether or not to clear the logger's {@link MockLogger.events} after performing the check.\n\t * Default: true.\n\t * @throws An error containing the actual/expected event data if the condition is not satisfied.\n\t */\n\tpublic assertMatchStrict(\n\t\texpectedEvents: Omit<ITelemetryBaseEvent, \"category\">[],\n\t\tmessage?: string,\n\t\tinlineDetailsProp: boolean = false,\n\t\tclearEventsAfterCheck: boolean = true,\n\t): void {\n\t\t// Use copy to ensure events aren't cleared out from under us before we (potentially) throw\n\t\tconst actualEvents = this.events;\n\n\t\tif (!this.matchEventStrict(expectedEvents, inlineDetailsProp, clearEventsAfterCheck)) {\n\t\t\tthrow new Error(`${message ?? \"Logs don't match\"}\nexpected:\n${JSON.stringify(expectedEvents)}\n\nactual:\n${JSON.stringify(actualEvents)}`);\n\t\t}\n\t}\n\n\t/**\n\t * Asserts {@link MockLogger.matchAnyEvent} is `false` for the given events.\n\t * @param disallowedEvents - The events expected to not appear.\n\t * @param message - Optional error message to include in the thrown error, if the condition is not satisfied.\n\t * @param inlineDetailsProp - true if the \"details\" property in the actual event should be extracted and inlined.\n\t * These event objects may be subsets of the logged events.\n\t * Note: category is omitted from the type because it's usually uninteresting and tedious to type.\n\t * @param clearEventsAfterCheck - Whether or not to clear the logger's {@link MockLogger.events} after performing the check.\n\t * Default: true.\n\t * @throws An error containing the actual/expected event data if the condition is not satisfied.\n\t */\n\tpublic assertMatchNone(\n\t\tdisallowedEvents: Omit<ITelemetryBaseEvent, \"category\">[],\n\t\tmessage?: string,\n\t\tinlineDetailsProp: boolean = false,\n\t\tclearEventsAfterCheck: boolean = true,\n\t): void {\n\t\t// Use copy to ensure events aren't cleared out from under us before we (potentially) throw\n\t\tconst actualEvents = this.events;\n\n\t\tif (this.matchAnyEvent(disallowedEvents, inlineDetailsProp, clearEventsAfterCheck)) {\n\t\t\tthrow new Error(`${message ?? \"Logs don't match\"}\ndisallowed events:\n${JSON.stringify(disallowedEvents)}\n\nactual:\n${JSON.stringify(actualEvents)}`);\n\t\t}\n\t}\n\n\tprivate getMatchedEventsCount(\n\t\texpectedEvents: Omit<ITelemetryBaseEvent, \"category\">[],\n\t\tinlineDetailsProp: boolean,\n\t\tclearEventsAfterCheck: boolean,\n\t): number {\n\t\tlet iExpectedEvent = 0;\n\t\tfor (const event of this._events) {\n\t\t\tif (\n\t\t\t\tiExpectedEvent < expectedEvents.length &&\n\t\t\t\tMockLogger.eventsMatch(event, expectedEvents[iExpectedEvent], inlineDetailsProp)\n\t\t\t) {\n\t\t\t\t// We found the next expected event; increment\n\t\t\t\t++iExpectedEvent;\n\t\t\t}\n\t\t}\n\n\t\t// Remove the events so far; next call will just compare subsequent events from here\n\t\tif (clearEventsAfterCheck) {\n\t\t\tthis.clear();\n\t\t}\n\n\t\t// Return the count of matched events.\n\t\treturn iExpectedEvent;\n\t}\n\n\t/**\n\t * Ensure the expected event is a strict subset of the actual event\n\t */\n\tprivate static eventsMatch(\n\t\tactual: ITelemetryBaseEvent,\n\t\texpected: Omit<ITelemetryBaseEvent, \"category\">,\n\t\tinlineDetailsProp: boolean,\n\t): boolean {\n\t\tconst { details, ...actualForMatching } = actual;\n\t\t// \"details\" is used in a lot of telemetry logs to group a bunch of properties together and stringify them.\n\t\t// Some of the properties in the expected event may be inside \"details\". So, if inlineDetailsProp is true,\n\t\t// extract the properties from \"details\" in the actual event and inline them in the actual event.\n\t\tif (inlineDetailsProp && details !== undefined) {\n\t\t\tassert(\n\t\t\t\ttypeof details === \"string\",\n\t\t\t\t0x6c9 /* Details should a JSON stringified string if inlineDetailsProp is true */,\n\t\t\t);\n\t\t\t// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment\n\t\t\tconst detailsExpanded = JSON.parse(details);\n\t\t\t// eslint-disable-next-line @typescript-eslint/no-unsafe-argument\n\t\t\treturn matchObjects({ ...actualForMatching, ...detailsExpanded }, expected);\n\t\t}\n\t\treturn matchObjects(actual, expected);\n\t}\n\n\t/**\n\t * Throws if any errors were logged\n\t */\n\tpublic assertNoErrors(message?: string, clearEventsAfterCheck: boolean = true): void {\n\t\tconst actualEvents = this.events;\n\t\tconst errors = actualEvents.filter((event) => event.category === \"error\");\n\t\tif (clearEventsAfterCheck) {\n\t\t\tthis.clear();\n\t\t}\n\t\tif (errors.length > 0) {\n\t\t\tthrow new Error(`${message ?? \"Errors found in logs\"}\n\nerror logs:\n${JSON.stringify(errors)}`);\n\t\t}\n\t}\n}\n\nfunction matchObjects(\n\tactual: ITelemetryPropertiesExt,\n\texpected: ITelemetryPropertiesExt,\n): boolean {\n\tfor (const [expectedKey, expectedValue] of Object.entries(expected)) {\n\t\tconst actualValue = actual[expectedKey];\n\t\tif (\n\t\t\t!Array.isArray(expectedValue) &&\n\t\t\texpectedValue !== null &&\n\t\t\ttypeof expectedValue === \"object\"\n\t\t) {\n\t\t\tif (\n\t\t\t\tArray.isArray(actualValue) ||\n\t\t\t\tactualValue === null ||\n\t\t\t\ttypeof actualValue !== \"object\" ||\n\t\t\t\t!matchObjects(\n\t\t\t\t\tactualValue as ITelemetryPropertiesExt,\n\t\t\t\t\texpectedValue as ITelemetryPropertiesExt,\n\t\t\t\t)\n\t\t\t) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t} else if (JSON.stringify(actualValue) !== JSON.stringify(expectedValue)) {\n\t\t\treturn false;\n\t\t}\n\t}\n\treturn true;\n}\n\n/**\n * Mock {@link ITelemetryLoggerExt} implementation.\n *\n * @remarks Can be created via {@link createMockLoggerExt}.\n *\n * @internal\n */\nexport interface IMockLoggerExt extends ITelemetryLoggerExt {\n\t/**\n\t * Gets the events that have been logged so far.\n\t */\n\tevents(): readonly ITelemetryEventExt[];\n}\n\n/**\n * Creates an {@link IMockLoggerExt}.\n *\n * @internal\n */\nexport function createMockLoggerExt(minLogLevel?: LogLevel): IMockLoggerExt {\n\tconst mockLogger = new MockLogger(minLogLevel);\n\tconst childLogger = createChildLogger({ logger: mockLogger });\n\tObject.assign(childLogger, {\n\t\tevents: (): readonly ITelemetryEventExt[] =>\n\t\t\tmockLogger.events.map((e) => e as ITelemetryEventExt),\n\t});\n\treturn childLogger as IMockLoggerExt;\n}\n"]}
1
+ {"version":3,"file":"mockLogger.js","sourceRoot":"","sources":["../src/mockLogger.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAGN,QAAQ,GACR,MAAM,iCAAiC,CAAC;AACzC,OAAO,EAAE,MAAM,EAAE,MAAM,qCAAqC,CAAC;AAE7D,OAAO,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAOhD;;;;;;;GAOG;AACH,MAAM,OAAO,UAAU;IACtB;;OAEG;IACH,IAAW,MAAM;QAChB,OAAO,CAAC,GAAG,IAAI,CAAC,OAAO,CAAC,CAAC;IAC1B,CAAC;IASD,YAAmB,WAAsB;QAPjC,YAAO,GAA0B,EAAE,CAAC;QAQ3C,IAAI,CAAC,WAAW,GAAG,WAAW,IAAI,QAAQ,CAAC,OAAO,CAAC;IACpD,CAAC;IAED;;OAEG;IACI,KAAK;QACX,IAAI,CAAC,OAAO,GAAG,EAAE,CAAC;IACnB,CAAC;IAEM,iBAAiB;QACvB,OAAO,iBAAiB,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC;IAC5C,CAAC;IAED;;OAEG;IACI,IAAI,CAAC,KAA0B,EAAE,QAAmB;QAC1D,IAAI,CAAC,QAAQ,IAAI,QAAQ,CAAC,OAAO,CAAC,IAAI,IAAI,CAAC,WAAW,EAAE,CAAC;YACxD,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAC1B,CAAC;IACF,CAAC;IAED;;;;;;;;;OASG;IACI,WAAW,CACjB,cAAuD,EACvD,oBAA6B,KAAK,EAClC,wBAAiC,IAAI;QAErC,MAAM,yBAAyB,GAAG,IAAI,CAAC,qBAAqB,CAC3D,cAAc,EACd,iBAAiB,EACjB,qBAAqB,CACrB,CAAC;QACF,2DAA2D;QAC3D,MAAM,2BAA2B,GAAG,cAAc,CAAC,MAAM,GAAG,yBAAyB,CAAC;QACtF,OAAO,2BAA2B,KAAK,CAAC,CAAC;IAC1C,CAAC;IAED;;;;;;;;;;OAUG;IACI,WAAW,CACjB,cAAuD,EACvD,OAAgB,EAChB,oBAA6B,KAAK,EAClC,wBAAiC,IAAI;QAErC,2FAA2F;QAC3F,MAAM,YAAY,GAAG,IAAI,CAAC,MAAM,CAAC;QAEjC,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC,cAAc,EAAE,iBAAiB,EAAE,qBAAqB,CAAC,EAAE,CAAC;YACjF,MAAM,IAAI,KAAK,CAAC,GAAG,OAAO,IAAI,kBAAkB;;EAEjD,IAAI,CAAC,SAAS,CAAC,cAAc,CAAC;;;EAG9B,IAAI,CAAC,SAAS,CAAC,YAAY,CAAC,EAAE,CAAC,CAAC;QAChC,CAAC;IACF,CAAC;IAED;;;;;;;;OAQG;IACI,aAAa,CACnB,cAAuD,EACvD,oBAA6B,KAAK,EAClC,wBAAiC,IAAI;QAErC,MAAM,yBAAyB,GAAG,IAAI,CAAC,qBAAqB,CAC3D,cAAc,EACd,iBAAiB,EACjB,qBAAqB,CACrB,CAAC;QACF,OAAO,yBAAyB,GAAG,CAAC,CAAC;IACtC,CAAC;IAED;;;;;;;;;;OAUG;IACI,cAAc,CACpB,cAAuD,EACvD,OAAgB,EAChB,oBAA6B,KAAK,EAClC,wBAAiC,IAAI;QAErC,2FAA2F;QAC3F,MAAM,YAAY,GAAG,IAAI,CAAC,MAAM,CAAC;QAEjC,IAAI,CAAC,IAAI,CAAC,aAAa,CAAC,cAAc,EAAE,iBAAiB,EAAE,qBAAqB,CAAC,EAAE,CAAC;YACnF,MAAM,IAAI,KAAK,CAAC,GAAG,OAAO,IAAI,kBAAkB;;EAEjD,IAAI,CAAC,SAAS,CAAC,cAAc,CAAC;;;EAG9B,IAAI,CAAC,SAAS,CAAC,YAAY,CAAC,EAAE,CAAC,CAAC;QAChC,CAAC;IACF,CAAC;IAED;;;;;;;;;OASG;IACI,gBAAgB,CACtB,cAAuD,EACvD,oBAA6B,KAAK,EAClC,wBAAiC,IAAI;QAErC,IAAI,cAAc,CAAC,MAAM,KAAK,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,CAAC;YACnD,IAAI,qBAAqB,EAAE,CAAC;gBAC3B,IAAI,CAAC,KAAK,EAAE,CAAC;YACd,CAAC;YACD,OAAO,KAAK,CAAC;QACd,CAAC;QAED,4DAA4D;QAC5D,OAAO,IAAI,CAAC,WAAW,CAAC,cAAc,EAAE,iBAAiB,EAAE,qBAAqB,CAAC,CAAC;IACnF,CAAC;IAED;;;;;;;;;;OAUG;IACI,iBAAiB,CACvB,cAAuD,EACvD,OAAgB,EAChB,oBAA6B,KAAK,EAClC,wBAAiC,IAAI;QAErC,2FAA2F;QAC3F,MAAM,YAAY,GAAG,IAAI,CAAC,MAAM,CAAC;QAEjC,IAAI,CAAC,IAAI,CAAC,gBAAgB,CAAC,cAAc,EAAE,iBAAiB,EAAE,qBAAqB,CAAC,EAAE,CAAC;YACtF,MAAM,IAAI,KAAK,CAAC,GAAG,OAAO,IAAI,kBAAkB;;EAEjD,IAAI,CAAC,SAAS,CAAC,cAAc,CAAC;;;EAG9B,IAAI,CAAC,SAAS,CAAC,YAAY,CAAC,EAAE,CAAC,CAAC;QAChC,CAAC;IACF,CAAC;IAED;;;;;;;;;;OAUG;IACI,eAAe,CACrB,gBAAyD,EACzD,OAAgB,EAChB,oBAA6B,KAAK,EAClC,wBAAiC,IAAI;QAErC,2FAA2F;QAC3F,MAAM,YAAY,GAAG,IAAI,CAAC,MAAM,CAAC;QAEjC,IAAI,IAAI,CAAC,aAAa,CAAC,gBAAgB,EAAE,iBAAiB,EAAE,qBAAqB,CAAC,EAAE,CAAC;YACpF,MAAM,IAAI,KAAK,CAAC,GAAG,OAAO,IAAI,kBAAkB;;EAEjD,IAAI,CAAC,SAAS,CAAC,gBAAgB,CAAC;;;EAGhC,IAAI,CAAC,SAAS,CAAC,YAAY,CAAC,EAAE,CAAC,CAAC;QAChC,CAAC;IACF,CAAC;IAEO,qBAAqB,CAC5B,cAAuD,EACvD,iBAA0B,EAC1B,qBAA8B;QAE9B,IAAI,cAAc,GAAG,CAAC,CAAC;QACvB,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;YAClC,MAAM,aAAa,GAAG,cAAc,CAAC,cAAc,CAAC,CAAC;YACrD,IACC,cAAc,GAAG,cAAc,CAAC,MAAM;gBACtC,aAAa,KAAK,SAAS;gBAC3B,UAAU,CAAC,WAAW,CAAC,KAAK,EAAE,aAAa,EAAE,iBAAiB,CAAC,EAC9D,CAAC;gBACF,8CAA8C;gBAC9C,EAAE,cAAc,CAAC;YAClB,CAAC;QACF,CAAC;QAED,oFAAoF;QACpF,IAAI,qBAAqB,EAAE,CAAC;YAC3B,IAAI,CAAC,KAAK,EAAE,CAAC;QACd,CAAC;QAED,sCAAsC;QACtC,OAAO,cAAc,CAAC;IACvB,CAAC;IAED;;OAEG;IACK,MAAM,CAAC,WAAW,CACzB,MAA2B,EAC3B,QAA+C,EAC/C,iBAA0B;QAE1B,MAAM,EAAE,OAAO,EAAE,GAAG,iBAAiB,EAAE,GAAG,MAAM,CAAC;QACjD,2GAA2G;QAC3G,0GAA0G;QAC1G,iGAAiG;QACjG,IAAI,iBAAiB,IAAI,OAAO,KAAK,SAAS,EAAE,CAAC;YAChD,MAAM,CACL,OAAO,OAAO,KAAK,QAAQ,EAC3B,KAAK,CAAC,2EAA2E,CACjF,CAAC;YACF,mEAAmE;YACnE,MAAM,eAAe,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;YAC5C,iEAAiE;YACjE,OAAO,YAAY,CAAC,EAAE,GAAG,iBAAiB,EAAE,GAAG,eAAe,EAAE,EAAE,QAAQ,CAAC,CAAC;QAC7E,CAAC;QACD,OAAO,YAAY,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;IACvC,CAAC;IAED;;OAEG;IACI,cAAc,CAAC,OAAgB,EAAE,wBAAiC,IAAI;QAC5E,MAAM,YAAY,GAAG,IAAI,CAAC,MAAM,CAAC;QACjC,MAAM,MAAM,GAAG,YAAY,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,QAAQ,KAAK,OAAO,CAAC,CAAC;QAC1E,IAAI,qBAAqB,EAAE,CAAC;YAC3B,IAAI,CAAC,KAAK,EAAE,CAAC;QACd,CAAC;QACD,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACvB,MAAM,IAAI,KAAK,CAAC,GAAG,OAAO,IAAI,sBAAsB;;;EAGrD,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;QAC1B,CAAC;IACF,CAAC;CACD;AAED,SAAS,YAAY,CACpB,MAA+B,EAC/B,QAAiC;IAEjC,KAAK,MAAM,CAAC,WAAW,EAAE,aAAa,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAC;QACrE,MAAM,WAAW,GAAG,MAAM,CAAC,WAAW,CAAC,CAAC;QACxC,IACC,CAAC,KAAK,CAAC,OAAO,CAAC,aAAa,CAAC;YAC7B,aAAa,KAAK,IAAI;YACtB,OAAO,aAAa,KAAK,QAAQ,EAChC,CAAC;YACF,IACC,KAAK,CAAC,OAAO,CAAC,WAAW,CAAC;gBAC1B,WAAW,KAAK,IAAI;gBACpB,OAAO,WAAW,KAAK,QAAQ;gBAC/B,CAAC,YAAY,CACZ,WAAsC,EACtC,aAAwC,CACxC,EACA,CAAC;gBACF,OAAO,KAAK,CAAC;YACd,CAAC;QACF,CAAC;aAAM,IAAI,IAAI,CAAC,SAAS,CAAC,WAAW,CAAC,KAAK,IAAI,CAAC,SAAS,CAAC,aAAa,CAAC,EAAE,CAAC;YAC1E,OAAO,KAAK,CAAC;QACd,CAAC;IACF,CAAC;IACD,OAAO,IAAI,CAAC;AACb,CAAC;AAgBD;;;;GAIG;AACH,MAAM,UAAU,mBAAmB,CAAC,WAAsB;IACzD,MAAM,UAAU,GAAG,IAAI,UAAU,CAAC,WAAW,CAAC,CAAC;IAC/C,MAAM,WAAW,GAAG,iBAAiB,CAAC,EAAE,MAAM,EAAE,UAAU,EAAE,CAAC,CAAC;IAC9D,MAAM,CAAC,MAAM,CAAC,WAAW,EAAE;QAC1B,MAAM,EAAE,GAAkC,EAAE,CAC3C,UAAU,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAuB,CAAC;KACtD,CAAC,CAAC;IACH,OAAO,WAA6B,CAAC;AACtC,CAAC","sourcesContent":["/*!\n * Copyright (c) Microsoft Corporation and contributors. All rights reserved.\n * Licensed under the MIT License.\n */\n\nimport {\n\ttype ITelemetryBaseEvent,\n\ttype ITelemetryBaseLogger,\n\tLogLevel,\n} from \"@fluidframework/core-interfaces\";\nimport { assert } from \"@fluidframework/core-utils/internal\";\n\nimport { createChildLogger } from \"./logger.js\";\nimport type {\n\tITelemetryEventExt,\n\tITelemetryLoggerExt,\n\tITelemetryPropertiesExt,\n} from \"./telemetryTypes.js\";\n\n/**\n * Mock {@link @fluidframework/core-interfaces#ITelemetryBaseLogger} implementation.\n *\n * Records events sent to it, and then can walk back over those events, searching for a set of expected events to\n * match against the logged events.\n *\n * @internal\n */\nexport class MockLogger implements ITelemetryBaseLogger {\n\t/**\n\t * Gets an immutable copy of the events logged thus far.\n\t */\n\tpublic get events(): readonly ITelemetryBaseEvent[] {\n\t\treturn [...this._events];\n\t}\n\n\tprivate _events: ITelemetryBaseEvent[] = [];\n\n\t/**\n\t * {@inheritDoc @fluidframework/core-interfaces#ITelemetryBaseLogger.minLogLevel}\n\t */\n\tpublic readonly minLogLevel: LogLevel;\n\n\tpublic constructor(minLogLevel?: LogLevel) {\n\t\tthis.minLogLevel = minLogLevel ?? LogLevel.default;\n\t}\n\n\t/**\n\t * Clears the events logged thus far.\n\t */\n\tpublic clear(): void {\n\t\tthis._events = [];\n\t}\n\n\tpublic toTelemetryLogger(): ITelemetryLoggerExt {\n\t\treturn createChildLogger({ logger: this });\n\t}\n\n\t/**\n\t * {@inheritDoc @fluidframework/core-interfaces#ITelemetryBaseLogger.send}\n\t */\n\tpublic send(event: ITelemetryBaseEvent, logLevel?: LogLevel): void {\n\t\tif ((logLevel ?? LogLevel.default) >= this.minLogLevel) {\n\t\t\tthis._events.push(event);\n\t\t}\n\t}\n\n\t/**\n\t * Search events logged since the last time matchEvents was called, looking for the given expected\n\t * events in order.\n\t * @param expectedEvents - events in order that are expected to appear in the recorded log.\n\t * @param inlineDetailsProp - true if the \"details\" property in the actual event should be extracted and inlined.\n\t * These event objects may be subsets of the logged events.\n\t * Note: category is omitted from the type because it's usually uninteresting and tedious to type.\n\t * @param clearEventsAfterCheck - Whether or not to clear the logger's {@link MockLogger.events} after performing the check.\n\t * Default: true.\n\t */\n\tpublic matchEvents(\n\t\texpectedEvents: Omit<ITelemetryBaseEvent, \"category\">[],\n\t\tinlineDetailsProp: boolean = false,\n\t\tclearEventsAfterCheck: boolean = true,\n\t): boolean {\n\t\tconst matchedExpectedEventCount = this.getMatchedEventsCount(\n\t\t\texpectedEvents,\n\t\t\tinlineDetailsProp,\n\t\t\tclearEventsAfterCheck,\n\t\t);\n\t\t// How many expected events were left over? Hopefully none.\n\t\tconst unmatchedExpectedEventCount = expectedEvents.length - matchedExpectedEventCount;\n\t\treturn unmatchedExpectedEventCount === 0;\n\t}\n\n\t/**\n\t * Asserts {@link MockLogger.matchEvents} is `true` for the given events.\n\t * @param expectedEvents - The events expected to appear.\n\t * @param message - Optional error message to include in the thrown error, if the condition is not satisfied.\n\t * @param inlineDetailsProp - true if the \"details\" property in the actual event should be extracted and inlined.\n\t * These event objects may be subsets of the logged events.\n\t * Note: category is omitted from the type because it's usually uninteresting and tedious to type.\n\t * @param clearEventsAfterCheck - Whether or not to clear the logger's {@link MockLogger.events} after performing the check.\n\t * Default: true.\n\t * @throws An error containing the actual/expected event data if the condition is not satisfied.\n\t */\n\tpublic assertMatch(\n\t\texpectedEvents: Omit<ITelemetryBaseEvent, \"category\">[],\n\t\tmessage?: string,\n\t\tinlineDetailsProp: boolean = false,\n\t\tclearEventsAfterCheck: boolean = true,\n\t): void {\n\t\t// Use copy to ensure events aren't cleared out from under us before we (potentially) throw\n\t\tconst actualEvents = this.events;\n\n\t\tif (!this.matchEvents(expectedEvents, inlineDetailsProp, clearEventsAfterCheck)) {\n\t\t\tthrow new Error(`${message ?? \"Logs don't match\"}\nexpected:\n${JSON.stringify(expectedEvents)}\n\nactual:\n${JSON.stringify(actualEvents)}`);\n\t\t}\n\t}\n\n\t/**\n\t * Search events logged since the last time matchEvents was called, looking for any of the given\n\t * expected events.\n\t * @param expectedEvents - events that are expected to appear in the recorded log.\n\t * @param inlineDetailsProp - true if the \"details\" property in the actual event should be extracted and inlined.\n\t * These event objects may be subsets of the logged events.\n\t * Note: category is omitted from the type because it's usually uninteresting and tedious to type.\n\t * @returns if any of the expected events is found.\n\t */\n\tpublic matchAnyEvent(\n\t\texpectedEvents: Omit<ITelemetryBaseEvent, \"category\">[],\n\t\tinlineDetailsProp: boolean = false,\n\t\tclearEventsAfterCheck: boolean = true,\n\t): boolean {\n\t\tconst matchedExpectedEventCount = this.getMatchedEventsCount(\n\t\t\texpectedEvents,\n\t\t\tinlineDetailsProp,\n\t\t\tclearEventsAfterCheck,\n\t\t);\n\t\treturn matchedExpectedEventCount > 0;\n\t}\n\n\t/**\n\t * Asserts {@link MockLogger.matchAnyEvent} is `true` for the given events.\n\t * @param expectedEvents - The events expected to appear.\n\t * @param message - Optional error message to include in the thrown error, if the condition is not satisfied.\n\t * @param inlineDetailsProp - true if the \"details\" property in the actual event should be extracted and inlined.\n\t * These event objects may be subsets of the logged events.\n\t * Note: category is omitted from the type because it's usually uninteresting and tedious to type.\n\t * @param clearEventsAfterCheck - Whether or not to clear the logger's {@link MockLogger.events} after performing the check.\n\t * Default: true.\n\t * @throws An error containing the actual/expected event data if the condition is not satisfied.\n\t */\n\tpublic assertMatchAny(\n\t\texpectedEvents: Omit<ITelemetryBaseEvent, \"category\">[],\n\t\tmessage?: string,\n\t\tinlineDetailsProp: boolean = false,\n\t\tclearEventsAfterCheck: boolean = true,\n\t): void {\n\t\t// Use copy to ensure events aren't cleared out from under us before we (potentially) throw\n\t\tconst actualEvents = this.events;\n\n\t\tif (!this.matchAnyEvent(expectedEvents, inlineDetailsProp, clearEventsAfterCheck)) {\n\t\t\tthrow new Error(`${message ?? \"Logs don't match\"}\nexpected:\n${JSON.stringify(expectedEvents)}\n\nactual:\n${JSON.stringify(actualEvents)}`);\n\t\t}\n\t}\n\n\t/**\n\t * Search events logged since the last time matchEvents was called, looking only for the given expected\n\t * events in order.\n\t * @param expectedEvents - events in order that are expected to be the only events in the recorded log.\n\t * @param inlineDetailsProp - true if the \"details\" property in the actual event should be extracted and inlined.\n\t * These event objects may be subsets of the logged events.\n\t * Note: category is omitted from the type because it's usually uninteresting and tedious to type.\n\t * @param clearEventsAfterCheck - Whether or not to clear the logger's {@link MockLogger.events} after performing the check.\n\t * Default: true.\n\t */\n\tpublic matchEventStrict(\n\t\texpectedEvents: Omit<ITelemetryBaseEvent, \"category\">[],\n\t\tinlineDetailsProp: boolean = false,\n\t\tclearEventsAfterCheck: boolean = true,\n\t): boolean {\n\t\tif (expectedEvents.length !== this._events.length) {\n\t\t\tif (clearEventsAfterCheck) {\n\t\t\t\tthis.clear();\n\t\t\t}\n\t\t\treturn false;\n\t\t}\n\n\t\t// `events` will be cleared by the below check if requested.\n\t\treturn this.matchEvents(expectedEvents, inlineDetailsProp, clearEventsAfterCheck);\n\t}\n\n\t/**\n\t * Asserts {@link MockLogger.matchEvents} is `true` for the given events.\n\t * @param expectedEvents - The events expected to appear.\n\t * @param message - Optional error message to include in the thrown error, if the condition is not satisfied.\n\t * @param inlineDetailsProp - true if the \"details\" property in the actual event should be extracted and inlined.\n\t * These event objects may be subsets of the logged events.\n\t * Note: category is omitted from the type because it's usually uninteresting and tedious to type.\n\t * @param clearEventsAfterCheck - Whether or not to clear the logger's {@link MockLogger.events} after performing the check.\n\t * Default: true.\n\t * @throws An error containing the actual/expected event data if the condition is not satisfied.\n\t */\n\tpublic assertMatchStrict(\n\t\texpectedEvents: Omit<ITelemetryBaseEvent, \"category\">[],\n\t\tmessage?: string,\n\t\tinlineDetailsProp: boolean = false,\n\t\tclearEventsAfterCheck: boolean = true,\n\t): void {\n\t\t// Use copy to ensure events aren't cleared out from under us before we (potentially) throw\n\t\tconst actualEvents = this.events;\n\n\t\tif (!this.matchEventStrict(expectedEvents, inlineDetailsProp, clearEventsAfterCheck)) {\n\t\t\tthrow new Error(`${message ?? \"Logs don't match\"}\nexpected:\n${JSON.stringify(expectedEvents)}\n\nactual:\n${JSON.stringify(actualEvents)}`);\n\t\t}\n\t}\n\n\t/**\n\t * Asserts {@link MockLogger.matchAnyEvent} is `false` for the given events.\n\t * @param disallowedEvents - The events expected to not appear.\n\t * @param message - Optional error message to include in the thrown error, if the condition is not satisfied.\n\t * @param inlineDetailsProp - true if the \"details\" property in the actual event should be extracted and inlined.\n\t * These event objects may be subsets of the logged events.\n\t * Note: category is omitted from the type because it's usually uninteresting and tedious to type.\n\t * @param clearEventsAfterCheck - Whether or not to clear the logger's {@link MockLogger.events} after performing the check.\n\t * Default: true.\n\t * @throws An error containing the actual/expected event data if the condition is not satisfied.\n\t */\n\tpublic assertMatchNone(\n\t\tdisallowedEvents: Omit<ITelemetryBaseEvent, \"category\">[],\n\t\tmessage?: string,\n\t\tinlineDetailsProp: boolean = false,\n\t\tclearEventsAfterCheck: boolean = true,\n\t): void {\n\t\t// Use copy to ensure events aren't cleared out from under us before we (potentially) throw\n\t\tconst actualEvents = this.events;\n\n\t\tif (this.matchAnyEvent(disallowedEvents, inlineDetailsProp, clearEventsAfterCheck)) {\n\t\t\tthrow new Error(`${message ?? \"Logs don't match\"}\ndisallowed events:\n${JSON.stringify(disallowedEvents)}\n\nactual:\n${JSON.stringify(actualEvents)}`);\n\t\t}\n\t}\n\n\tprivate getMatchedEventsCount(\n\t\texpectedEvents: Omit<ITelemetryBaseEvent, \"category\">[],\n\t\tinlineDetailsProp: boolean,\n\t\tclearEventsAfterCheck: boolean,\n\t): number {\n\t\tlet iExpectedEvent = 0;\n\t\tfor (const event of this._events) {\n\t\t\tconst expectedEvent = expectedEvents[iExpectedEvent];\n\t\t\tif (\n\t\t\t\tiExpectedEvent < expectedEvents.length &&\n\t\t\t\texpectedEvent !== undefined &&\n\t\t\t\tMockLogger.eventsMatch(event, expectedEvent, inlineDetailsProp)\n\t\t\t) {\n\t\t\t\t// We found the next expected event; increment\n\t\t\t\t++iExpectedEvent;\n\t\t\t}\n\t\t}\n\n\t\t// Remove the events so far; next call will just compare subsequent events from here\n\t\tif (clearEventsAfterCheck) {\n\t\t\tthis.clear();\n\t\t}\n\n\t\t// Return the count of matched events.\n\t\treturn iExpectedEvent;\n\t}\n\n\t/**\n\t * Ensure the expected event is a strict subset of the actual event\n\t */\n\tprivate static eventsMatch(\n\t\tactual: ITelemetryBaseEvent,\n\t\texpected: Omit<ITelemetryBaseEvent, \"category\">,\n\t\tinlineDetailsProp: boolean,\n\t): boolean {\n\t\tconst { details, ...actualForMatching } = actual;\n\t\t// \"details\" is used in a lot of telemetry logs to group a bunch of properties together and stringify them.\n\t\t// Some of the properties in the expected event may be inside \"details\". So, if inlineDetailsProp is true,\n\t\t// extract the properties from \"details\" in the actual event and inline them in the actual event.\n\t\tif (inlineDetailsProp && details !== undefined) {\n\t\t\tassert(\n\t\t\t\ttypeof details === \"string\",\n\t\t\t\t0x6c9 /* Details should a JSON stringified string if inlineDetailsProp is true */,\n\t\t\t);\n\t\t\t// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment\n\t\t\tconst detailsExpanded = JSON.parse(details);\n\t\t\t// eslint-disable-next-line @typescript-eslint/no-unsafe-argument\n\t\t\treturn matchObjects({ ...actualForMatching, ...detailsExpanded }, expected);\n\t\t}\n\t\treturn matchObjects(actual, expected);\n\t}\n\n\t/**\n\t * Throws if any errors were logged\n\t */\n\tpublic assertNoErrors(message?: string, clearEventsAfterCheck: boolean = true): void {\n\t\tconst actualEvents = this.events;\n\t\tconst errors = actualEvents.filter((event) => event.category === \"error\");\n\t\tif (clearEventsAfterCheck) {\n\t\t\tthis.clear();\n\t\t}\n\t\tif (errors.length > 0) {\n\t\t\tthrow new Error(`${message ?? \"Errors found in logs\"}\n\nerror logs:\n${JSON.stringify(errors)}`);\n\t\t}\n\t}\n}\n\nfunction matchObjects(\n\tactual: ITelemetryPropertiesExt,\n\texpected: ITelemetryPropertiesExt,\n): boolean {\n\tfor (const [expectedKey, expectedValue] of Object.entries(expected)) {\n\t\tconst actualValue = actual[expectedKey];\n\t\tif (\n\t\t\t!Array.isArray(expectedValue) &&\n\t\t\texpectedValue !== null &&\n\t\t\ttypeof expectedValue === \"object\"\n\t\t) {\n\t\t\tif (\n\t\t\t\tArray.isArray(actualValue) ||\n\t\t\t\tactualValue === null ||\n\t\t\t\ttypeof actualValue !== \"object\" ||\n\t\t\t\t!matchObjects(\n\t\t\t\t\tactualValue as ITelemetryPropertiesExt,\n\t\t\t\t\texpectedValue as ITelemetryPropertiesExt,\n\t\t\t\t)\n\t\t\t) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t} else if (JSON.stringify(actualValue) !== JSON.stringify(expectedValue)) {\n\t\t\treturn false;\n\t\t}\n\t}\n\treturn true;\n}\n\n/**\n * Mock {@link ITelemetryLoggerExt} implementation.\n *\n * @remarks Can be created via {@link createMockLoggerExt}.\n *\n * @internal\n */\nexport interface IMockLoggerExt extends ITelemetryLoggerExt {\n\t/**\n\t * Gets the events that have been logged so far.\n\t */\n\tevents(): readonly ITelemetryEventExt[];\n}\n\n/**\n * Creates an {@link IMockLoggerExt}.\n *\n * @internal\n */\nexport function createMockLoggerExt(minLogLevel?: LogLevel): IMockLoggerExt {\n\tconst mockLogger = new MockLogger(minLogLevel);\n\tconst childLogger = createChildLogger({ logger: mockLogger });\n\tObject.assign(childLogger, {\n\t\tevents: (): readonly ITelemetryEventExt[] =>\n\t\t\tmockLogger.events.map((e) => e as ITelemetryEventExt),\n\t});\n\treturn childLogger as IMockLoggerExt;\n}\n"]}
@@ -2,7 +2,7 @@
2
2
  * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
3
3
  * Licensed under the MIT License.
4
4
  */
5
- import { performance } from "@fluid-internal/client-utils";
5
+ import { performanceNow } from "@fluid-internal/client-utils";
6
6
  import { assert } from "@fluidframework/core-utils/internal";
7
7
  import { roundToDecimalPlaces } from "./mathTools.js";
8
8
  /**
@@ -72,9 +72,9 @@ export class SampledTelemetryHelper {
72
72
  * @returns Whatever the passed-in code block returns.
73
73
  */
74
74
  measure(codeToMeasure, bucket = "") {
75
- const start = performance.now();
75
+ const start = performanceNow();
76
76
  const returnValue = codeToMeasure();
77
- const duration = performance.now() - start;
77
+ const duration = performanceNow() - start;
78
78
  let loggerData = this.measurementsMap.get(bucket);
79
79
  if (loggerData === undefined) {
80
80
  loggerData = {
@@ -1 +1 @@
1
- {"version":3,"file":"sampledTelemetryHelper.js","sourceRoot":"","sources":["../src/sampledTelemetryHelper.ts"],"names":[],"mappings":"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","sourcesContent":["/*!\n * Copyright (c) Microsoft Corporation and contributors. All rights reserved.\n * Licensed under the MIT License.\n */\n\nimport { performance } from \"@fluid-internal/client-utils\";\nimport type { IDisposable, ITelemetryBaseProperties } from \"@fluidframework/core-interfaces\";\nimport { assert } from \"@fluidframework/core-utils/internal\";\n\nimport { roundToDecimalPlaces } from \"./mathTools.js\";\nimport type {\n\tITelemetryGenericEventExt,\n\tITelemetryLoggerExt,\n\tITelemetryPerformanceEventExt,\n} from \"./telemetryTypes.js\";\n\n/**\n * @privateRemarks\n *\n * The names of the properties in this interface are the ones that will get stamped in the\n * telemetry event, changes should be considered carefully. The optional properties should\n * only be populated if 'includeAggregateMetrics' is true.\n */\ninterface Measurements {\n\t/**\n\t * The duration of the latest execution.\n\t */\n\tduration: number;\n\n\t/**\n\t * The number of executions since the last time an event was generated.\n\t */\n\tcount: number;\n\n\t/**\n\t * Total duration across all the executions since the last event was generated.\n\t */\n\ttotalDuration?: number;\n\n\t/**\n\t * Min duration across all the executions since the last event was generated.\n\t */\n\tminDuration?: number;\n\n\t/**\n\t * Max duration across all the executions since the last event was generated.\n\t */\n\tmaxDuration?: number;\n\n\t/**\n\t * Average duration across all the executions since the last event was generated.\n\t */\n\taverageDuration?: number;\n}\n\n/**\n * The data that will be logged in the telemetry event.\n */\ninterface LoggerData {\n\tmeasurements: Measurements;\n\n\t/**\n\t * The sum of the custom data passed into the logger for each key.\n\t * Absence of a given key should be interpreted as 0.\n\t */\n\tdataSums: Record<string, number>;\n\n\t/**\n\t * The max of the custom data passed into the logger for each key.\n\t */\n\tdataMaxes: Record<string, number>;\n}\n\n/**\n * Helper type for an object whose properties are all numbers\n *\n * @internal\n */\nexport type CustomMetrics<TKey> = {\n\t[K in keyof TKey]: K extends string ? number : never;\n};\n\n/**\n * Potentially part of the structure of the return value of the function provided to {@link SampledTelemetryHelper.measure}.\n *\n * @see {@link MeasureReturnType} for more details on how this type is used.\n *\n * @internal\n */\nexport interface ICustomData<T> {\n\tcustomData: CustomMetrics<T>;\n}\n\n/**\n * Encapsulates the type-level logic for {@link SampledTelemetryHelper.measure}, to determine the expected return type\n * for the function that method receives (and by extension, its own return type). In words: {@link SampledTelemetryHelper}\n * is optionally provided with two generic types: one for custom metrics, and one for the actual return value of the\n * code that will be measured.\n *\n * - If no generic type is provided for custom metrics, then this type is simply the generic type provided for the actual\n * return value of the measured code (which could be void!).\n * - If a generic type is provided for custom metrics, then this type has a `customData` property whose type matches that\n * generic. Then if the generic type for the actual return value is not void, this type also has a property `returnValue`\n * whose type matches the generic type for the actual return value; if the generic type for the actual return value is\n * void, then this type _forbids_ a `returnValue` property (technically, it can exist but must be undefined in that case),\n * to try to ensure that the caller doesn't accidentally provide a function that actually returns a value.\n *\n * @internal\n */\nexport type MeasureReturnType<TMeasureReturn, TCustomMetrics> = TCustomMetrics extends void\n\t? TMeasureReturn\n\t: ICustomData<TCustomMetrics> &\n\t\t\t(TMeasureReturn extends void\n\t\t\t\t? { [K in \"returnValue\"]?: never }\n\t\t\t\t: { returnValue: TMeasureReturn });\n\n/**\n * Helper class that executes a specified code block and writes an\n * {@link @fluidframework/core-interfaces#ITelemetryPerformanceEvent} to a specified logger every time a specified\n * number of executions is reached (or when the class is disposed).\n *\n * @remarks\n * The `duration` field in the telemetry event this class generates is the duration of the latest execution (sample)\n * of the specified code block.\n * See the documentation of the `includeAggregateMetrics` parameter for additional details that can be included.\n *\n * @typeParam TMeasurementReturn - The return type (in a vacuum) of the code block that will be measured, ignoring\n * any custom metric data that might be required by this class. E.g., the code might just return a boolean.\n * @typeParam TCustomMetrics - A type that contains the custom properties that will be used by an instance of this class\n * for custom metrics. Each property in this type should be a number.\n *\n * @internal\n */\nexport class SampledTelemetryHelper<\n\tTMeasureReturn = void,\n\tTCustomMetrics extends CustomMetrics<TCustomMetrics> = void,\n> implements IDisposable\n{\n\tprivate _disposed: boolean = false;\n\n\t/**\n\t * {@inheritDoc @fluidframework/core-interfaces#IDisposable.disposed}\n\t */\n\tpublic get disposed(): boolean {\n\t\treturn this._disposed;\n\t}\n\n\tprivate readonly measurementsMap = new Map<string, LoggerData>();\n\n\t/**\n\t * @param eventBase -\n\t * Custom properties to include in the telemetry performance event when it is written.\n\t * @param logger -\n\t * The logger to use to write the telemetry performance event.\n\t * @param sampleThreshold -\n\t * Telemetry performance events will be generated every time we hit this many executions of the code block.\n\t * @param includeAggregateMetrics -\n\t * If set to `true`, the telemetry performance event will include aggregated metrics (total duration, min duration,\n\t * max duration) for all the executions in between generated events.\n\t * @param perBucketProperties -\n\t * Map of strings that represent different buckets (which can be specified when calling the 'measure' method), to\n\t * properties which should be added to the telemetry event for that bucket. If a bucket being measured does not\n\t * have an entry in this map, no additional properties will be added to its telemetry events. The following keys are\n\t * reserved for use by this class: \"duration\", \"count\", \"totalDuration\", \"minDuration\", \"maxDuration\". If any of\n\t * them is specified as a key in one of the ITelemetryBaseProperties objects in this map, that key-value pair will be\n\t * ignored.\n\t */\n\tpublic constructor(\n\t\tprivate readonly eventBase: ITelemetryGenericEventExt,\n\t\tprivate readonly logger: ITelemetryLoggerExt,\n\t\tprivate readonly sampleThreshold: number,\n\t\tprivate readonly includeAggregateMetrics: boolean = false,\n\t\tprivate readonly perBucketProperties = new Map<string, ITelemetryBaseProperties>(),\n\t) {}\n\n\t/**\n\t * Executes the specified code and keeps track of execution time statistics.\n\t * When it's been called enough times (the sampleThreshold for the class) then it generates a log message with the\n\t * necessary information.\n\t *\n\t * @remarks It's the responsibility of the caller to ensure that the same same set of custom metric properties is\n\t * provided each time this method is called on a given instance of {@link SampledTelemetryHelper}.\n\t * Otherwise the final measurements in the telemetry event may not be accurate.\n\t *\n\t * @param codeToMeasure - The code to be executed and measured.\n\t * @param bucket - A key to track executions of the code block separately.\n\t * Each different value of this parameter has a separate set of executions and metrics tracked by the class.\n\t * If no such distinction needs to be made, do not provide a value.\n\t * @returns Whatever the passed-in code block returns.\n\t */\n\tpublic measure(\n\t\tcodeToMeasure: () => MeasureReturnType<TMeasureReturn, TCustomMetrics>,\n\t\tbucket: string = \"\",\n\t): MeasureReturnType<TMeasureReturn, TCustomMetrics> {\n\t\tconst start = performance.now();\n\t\tconst returnValue = codeToMeasure();\n\t\tconst duration = performance.now() - start;\n\n\t\tlet loggerData = this.measurementsMap.get(bucket);\n\t\tif (loggerData === undefined) {\n\t\t\tloggerData = {\n\t\t\t\tmeasurements: { count: 0, duration: -1 },\n\t\t\t\tdataSums: {},\n\t\t\t\tdataMaxes: {},\n\t\t\t};\n\t\t\tthis.measurementsMap.set(bucket, loggerData);\n\t\t}\n\n\t\tconst m = loggerData.measurements;\n\t\tm.count++;\n\t\tm.duration = duration;\n\n\t\tif (this.includeAggregateMetrics) {\n\t\t\tm.totalDuration = (m.totalDuration ?? 0) + duration;\n\t\t\tm.minDuration = Math.min(m.minDuration ?? duration, duration);\n\t\t\tm.maxDuration = Math.max(m.maxDuration ?? 0, duration);\n\t\t}\n\n\t\tif (this.isCustomData(returnValue)) {\n\t\t\tloggerData = this.accumulateCustomData(returnValue.customData, loggerData);\n\t\t}\n\n\t\tif (m.count >= this.sampleThreshold) {\n\t\t\t// Computed separately to avoid multiple division operations.\n\t\t\tif (this.includeAggregateMetrics) {\n\t\t\t\tm.averageDuration = (m.totalDuration ?? 0) / m.count;\n\t\t\t}\n\t\t\tthis.flushBucket(bucket);\n\t\t}\n\n\t\treturn returnValue;\n\t}\n\n\tprivate isCustomData(data: unknown): data is ICustomData<TCustomMetrics> {\n\t\treturn (\n\t\t\ttypeof data === \"object\" &&\n\t\t\tdata !== null &&\n\t\t\t\"customData\" in data &&\n\t\t\ttypeof data.customData === \"object\"\n\t\t);\n\t}\n\n\tprivate accumulateCustomData(\n\t\tcustomData: CustomMetrics<TCustomMetrics>,\n\t\tloggerData: LoggerData,\n\t): LoggerData {\n\t\tfor (const [key, val] of Object.entries(customData)) {\n\t\t\tassert(typeof key === \"string\", 0x9df /* Key should be a string */);\n\t\t\tassert(typeof val === \"number\", 0x9e0 /* Value should be a number */);\n\n\t\t\tloggerData.dataSums[key] = (loggerData.dataSums[key] ?? 0) + val;\n\t\t\tloggerData.dataMaxes[key] = Math.max(\n\t\t\t\tloggerData.dataMaxes[key] ?? Number.NEGATIVE_INFINITY,\n\t\t\t\tval,\n\t\t\t);\n\t\t}\n\n\t\treturn loggerData;\n\t}\n\n\tprivate processCustomData(loggerData: LoggerData, count: number): Record<string, number> {\n\t\tconst processedCustomData: Record<string, number> = {};\n\n\t\tif (loggerData.dataSums === undefined || loggerData.dataMaxes === undefined) {\n\t\t\treturn processedCustomData;\n\t\t}\n\n\t\tconst dataSums = loggerData.dataSums;\n\t\tconst dataMaxes = loggerData.dataMaxes;\n\n\t\tfor (const [key, val] of Object.entries(dataSums)) {\n\t\t\t// implementation of class guarantees the keys between dataMaxes and dataSums align.\n\t\t\tprocessedCustomData[`avg_${key}`] = roundToDecimalPlaces(val / count, 6);\n\t\t\tprocessedCustomData[`max_${key}`] = dataMaxes[key] ?? 0;\n\t\t}\n\n\t\treturn processedCustomData;\n\t}\n\n\tprivate flushBucket(bucket: string): void {\n\t\tconst loggerData = this.measurementsMap.get(bucket);\n\t\tif (loggerData === undefined) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst measurements = loggerData.measurements;\n\n\t\tconst processedCustomData = this.processCustomData(loggerData, measurements.count);\n\n\t\tif (measurements.count !== 0) {\n\t\t\tconst bucketProperties = this.perBucketProperties.get(bucket);\n\n\t\t\tconst telemetryEvent: ITelemetryPerformanceEventExt = {\n\t\t\t\t...this.eventBase,\n\t\t\t\t...bucketProperties, // If the bucket doesn't exist and this is undefined, things work as expected\n\t\t\t\t...measurements,\n\t\t\t\t...processedCustomData,\n\t\t\t};\n\n\t\t\tthis.logger.sendPerformanceEvent(telemetryEvent);\n\t\t\tthis.measurementsMap.delete(bucket);\n\t\t}\n\t}\n\n\tpublic dispose(error?: Error | undefined): void {\n\t\tfor (const [k] of this.measurementsMap.entries()) {\n\t\t\tthis.flushBucket(k);\n\t\t}\n\t\tthis._disposed = true;\n\t}\n}\n"]}
1
+ {"version":3,"file":"sampledTelemetryHelper.js","sourceRoot":"","sources":["../src/sampledTelemetryHelper.ts"],"names":[],"mappings":"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","sourcesContent":["/*!\n * Copyright (c) Microsoft Corporation and contributors. All rights reserved.\n * Licensed under the MIT License.\n */\n\nimport { performanceNow } from \"@fluid-internal/client-utils\";\nimport type { IDisposable, ITelemetryBaseProperties } from \"@fluidframework/core-interfaces\";\nimport { assert } from \"@fluidframework/core-utils/internal\";\n\nimport { roundToDecimalPlaces } from \"./mathTools.js\";\nimport type {\n\tITelemetryGenericEventExt,\n\tITelemetryLoggerExt,\n\tITelemetryPerformanceEventExt,\n} from \"./telemetryTypes.js\";\n\n/**\n * @privateRemarks\n *\n * The names of the properties in this interface are the ones that will get stamped in the\n * telemetry event, changes should be considered carefully. The optional properties should\n * only be populated if 'includeAggregateMetrics' is true.\n */\ninterface Measurements {\n\t/**\n\t * The duration of the latest execution.\n\t */\n\tduration: number;\n\n\t/**\n\t * The number of executions since the last time an event was generated.\n\t */\n\tcount: number;\n\n\t/**\n\t * Total duration across all the executions since the last event was generated.\n\t */\n\ttotalDuration?: number;\n\n\t/**\n\t * Min duration across all the executions since the last event was generated.\n\t */\n\tminDuration?: number;\n\n\t/**\n\t * Max duration across all the executions since the last event was generated.\n\t */\n\tmaxDuration?: number;\n\n\t/**\n\t * Average duration across all the executions since the last event was generated.\n\t */\n\taverageDuration?: number;\n}\n\n/**\n * The data that will be logged in the telemetry event.\n */\ninterface LoggerData {\n\tmeasurements: Measurements;\n\n\t/**\n\t * The sum of the custom data passed into the logger for each key.\n\t * Absence of a given key should be interpreted as 0.\n\t */\n\tdataSums: Record<string, number>;\n\n\t/**\n\t * The max of the custom data passed into the logger for each key.\n\t */\n\tdataMaxes: Record<string, number>;\n}\n\n/**\n * Helper type for an object whose properties are all numbers\n *\n * @internal\n */\nexport type CustomMetrics<TKey> = {\n\t[K in keyof TKey]: K extends string ? number : never;\n};\n\n/**\n * Potentially part of the structure of the return value of the function provided to {@link SampledTelemetryHelper.measure}.\n *\n * @see {@link MeasureReturnType} for more details on how this type is used.\n *\n * @internal\n */\nexport interface ICustomData<T> {\n\tcustomData: CustomMetrics<T>;\n}\n\n/**\n * Encapsulates the type-level logic for {@link SampledTelemetryHelper.measure}, to determine the expected return type\n * for the function that method receives (and by extension, its own return type). In words: {@link SampledTelemetryHelper}\n * is optionally provided with two generic types: one for custom metrics, and one for the actual return value of the\n * code that will be measured.\n *\n * - If no generic type is provided for custom metrics, then this type is simply the generic type provided for the actual\n * return value of the measured code (which could be void!).\n * - If a generic type is provided for custom metrics, then this type has a `customData` property whose type matches that\n * generic. Then if the generic type for the actual return value is not void, this type also has a property `returnValue`\n * whose type matches the generic type for the actual return value; if the generic type for the actual return value is\n * void, then this type _forbids_ a `returnValue` property (technically, it can exist but must be undefined in that case),\n * to try to ensure that the caller doesn't accidentally provide a function that actually returns a value.\n *\n * @internal\n */\nexport type MeasureReturnType<TMeasureReturn, TCustomMetrics> = TCustomMetrics extends void\n\t? TMeasureReturn\n\t: ICustomData<TCustomMetrics> &\n\t\t\t(TMeasureReturn extends void\n\t\t\t\t? { [K in \"returnValue\"]?: never }\n\t\t\t\t: { returnValue: TMeasureReturn });\n\n/**\n * Helper class that executes a specified code block and writes an\n * {@link @fluidframework/core-interfaces#ITelemetryPerformanceEvent} to a specified logger every time a specified\n * number of executions is reached (or when the class is disposed).\n *\n * @remarks\n * The `duration` field in the telemetry event this class generates is the duration of the latest execution (sample)\n * of the specified code block.\n * See the documentation of the `includeAggregateMetrics` parameter for additional details that can be included.\n *\n * @typeParam TMeasurementReturn - The return type (in a vacuum) of the code block that will be measured, ignoring\n * any custom metric data that might be required by this class. E.g., the code might just return a boolean.\n * @typeParam TCustomMetrics - A type that contains the custom properties that will be used by an instance of this class\n * for custom metrics. Each property in this type should be a number.\n *\n * @internal\n */\nexport class SampledTelemetryHelper<\n\tTMeasureReturn = void,\n\tTCustomMetrics extends CustomMetrics<TCustomMetrics> = void,\n> implements IDisposable\n{\n\tprivate _disposed: boolean = false;\n\n\t/**\n\t * {@inheritDoc @fluidframework/core-interfaces#IDisposable.disposed}\n\t */\n\tpublic get disposed(): boolean {\n\t\treturn this._disposed;\n\t}\n\n\tprivate readonly measurementsMap = new Map<string, LoggerData>();\n\n\t/**\n\t * @param eventBase -\n\t * Custom properties to include in the telemetry performance event when it is written.\n\t * @param logger -\n\t * The logger to use to write the telemetry performance event.\n\t * @param sampleThreshold -\n\t * Telemetry performance events will be generated every time we hit this many executions of the code block.\n\t * @param includeAggregateMetrics -\n\t * If set to `true`, the telemetry performance event will include aggregated metrics (total duration, min duration,\n\t * max duration) for all the executions in between generated events.\n\t * @param perBucketProperties -\n\t * Map of strings that represent different buckets (which can be specified when calling the 'measure' method), to\n\t * properties which should be added to the telemetry event for that bucket. If a bucket being measured does not\n\t * have an entry in this map, no additional properties will be added to its telemetry events. The following keys are\n\t * reserved for use by this class: \"duration\", \"count\", \"totalDuration\", \"minDuration\", \"maxDuration\". If any of\n\t * them is specified as a key in one of the ITelemetryBaseProperties objects in this map, that key-value pair will be\n\t * ignored.\n\t */\n\tpublic constructor(\n\t\tprivate readonly eventBase: ITelemetryGenericEventExt,\n\t\tprivate readonly logger: ITelemetryLoggerExt,\n\t\tprivate readonly sampleThreshold: number,\n\t\tprivate readonly includeAggregateMetrics: boolean = false,\n\t\tprivate readonly perBucketProperties = new Map<string, ITelemetryBaseProperties>(),\n\t) {}\n\n\t/**\n\t * Executes the specified code and keeps track of execution time statistics.\n\t * When it's been called enough times (the sampleThreshold for the class) then it generates a log message with the\n\t * necessary information.\n\t *\n\t * @remarks It's the responsibility of the caller to ensure that the same same set of custom metric properties is\n\t * provided each time this method is called on a given instance of {@link SampledTelemetryHelper}.\n\t * Otherwise the final measurements in the telemetry event may not be accurate.\n\t *\n\t * @param codeToMeasure - The code to be executed and measured.\n\t * @param bucket - A key to track executions of the code block separately.\n\t * Each different value of this parameter has a separate set of executions and metrics tracked by the class.\n\t * If no such distinction needs to be made, do not provide a value.\n\t * @returns Whatever the passed-in code block returns.\n\t */\n\tpublic measure(\n\t\tcodeToMeasure: () => MeasureReturnType<TMeasureReturn, TCustomMetrics>,\n\t\tbucket: string = \"\",\n\t): MeasureReturnType<TMeasureReturn, TCustomMetrics> {\n\t\tconst start = performanceNow();\n\t\tconst returnValue = codeToMeasure();\n\t\tconst duration = performanceNow() - start;\n\n\t\tlet loggerData = this.measurementsMap.get(bucket);\n\t\tif (loggerData === undefined) {\n\t\t\tloggerData = {\n\t\t\t\tmeasurements: { count: 0, duration: -1 },\n\t\t\t\tdataSums: {},\n\t\t\t\tdataMaxes: {},\n\t\t\t};\n\t\t\tthis.measurementsMap.set(bucket, loggerData);\n\t\t}\n\n\t\tconst m = loggerData.measurements;\n\t\tm.count++;\n\t\tm.duration = duration;\n\n\t\tif (this.includeAggregateMetrics) {\n\t\t\tm.totalDuration = (m.totalDuration ?? 0) + duration;\n\t\t\tm.minDuration = Math.min(m.minDuration ?? duration, duration);\n\t\t\tm.maxDuration = Math.max(m.maxDuration ?? 0, duration);\n\t\t}\n\n\t\tif (this.isCustomData(returnValue)) {\n\t\t\tloggerData = this.accumulateCustomData(returnValue.customData, loggerData);\n\t\t}\n\n\t\tif (m.count >= this.sampleThreshold) {\n\t\t\t// Computed separately to avoid multiple division operations.\n\t\t\tif (this.includeAggregateMetrics) {\n\t\t\t\tm.averageDuration = (m.totalDuration ?? 0) / m.count;\n\t\t\t}\n\t\t\tthis.flushBucket(bucket);\n\t\t}\n\n\t\treturn returnValue;\n\t}\n\n\tprivate isCustomData(data: unknown): data is ICustomData<TCustomMetrics> {\n\t\treturn (\n\t\t\ttypeof data === \"object\" &&\n\t\t\tdata !== null &&\n\t\t\t\"customData\" in data &&\n\t\t\ttypeof data.customData === \"object\"\n\t\t);\n\t}\n\n\tprivate accumulateCustomData(\n\t\tcustomData: CustomMetrics<TCustomMetrics>,\n\t\tloggerData: LoggerData,\n\t): LoggerData {\n\t\tfor (const [key, val] of Object.entries(customData)) {\n\t\t\tassert(typeof key === \"string\", 0x9df /* Key should be a string */);\n\t\t\tassert(typeof val === \"number\", 0x9e0 /* Value should be a number */);\n\n\t\t\tloggerData.dataSums[key] = (loggerData.dataSums[key] ?? 0) + val;\n\t\t\tloggerData.dataMaxes[key] = Math.max(\n\t\t\t\tloggerData.dataMaxes[key] ?? Number.NEGATIVE_INFINITY,\n\t\t\t\tval,\n\t\t\t);\n\t\t}\n\n\t\treturn loggerData;\n\t}\n\n\tprivate processCustomData(loggerData: LoggerData, count: number): Record<string, number> {\n\t\tconst processedCustomData: Record<string, number> = {};\n\n\t\tif (loggerData.dataSums === undefined || loggerData.dataMaxes === undefined) {\n\t\t\treturn processedCustomData;\n\t\t}\n\n\t\tconst dataSums = loggerData.dataSums;\n\t\tconst dataMaxes = loggerData.dataMaxes;\n\n\t\tfor (const [key, val] of Object.entries(dataSums)) {\n\t\t\t// implementation of class guarantees the keys between dataMaxes and dataSums align.\n\t\t\tprocessedCustomData[`avg_${key}`] = roundToDecimalPlaces(val / count, 6);\n\t\t\tprocessedCustomData[`max_${key}`] = dataMaxes[key] ?? 0;\n\t\t}\n\n\t\treturn processedCustomData;\n\t}\n\n\tprivate flushBucket(bucket: string): void {\n\t\tconst loggerData = this.measurementsMap.get(bucket);\n\t\tif (loggerData === undefined) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst measurements = loggerData.measurements;\n\n\t\tconst processedCustomData = this.processCustomData(loggerData, measurements.count);\n\n\t\tif (measurements.count !== 0) {\n\t\t\tconst bucketProperties = this.perBucketProperties.get(bucket);\n\n\t\t\tconst telemetryEvent: ITelemetryPerformanceEventExt = {\n\t\t\t\t...this.eventBase,\n\t\t\t\t...bucketProperties, // If the bucket doesn't exist and this is undefined, things work as expected\n\t\t\t\t...measurements,\n\t\t\t\t...processedCustomData,\n\t\t\t};\n\n\t\t\tthis.logger.sendPerformanceEvent(telemetryEvent);\n\t\t\tthis.measurementsMap.delete(bucket);\n\t\t}\n\t}\n\n\tpublic dispose(error?: Error | undefined): void {\n\t\tfor (const [k] of this.measurementsMap.entries()) {\n\t\t\tthis.flushBucket(k);\n\t\t}\n\t\tthis._disposed = true;\n\t}\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fluidframework/telemetry-utils",
3
- "version": "2.20.0",
3
+ "version": "2.22.0",
4
4
  "description": "Collection of telemetry relates utilities for Fluid",
5
5
  "homepage": "https://fluidframework.com",
6
6
  "repository": {
@@ -69,22 +69,22 @@
69
69
  "temp-directory": "nyc/.nyc_output"
70
70
  },
71
71
  "dependencies": {
72
- "@fluid-internal/client-utils": "~2.20.0",
73
- "@fluidframework/core-interfaces": "~2.20.0",
74
- "@fluidframework/core-utils": "~2.20.0",
75
- "@fluidframework/driver-definitions": "~2.20.0",
72
+ "@fluid-internal/client-utils": "~2.22.0",
73
+ "@fluidframework/core-interfaces": "~2.22.0",
74
+ "@fluidframework/core-utils": "~2.22.0",
75
+ "@fluidframework/driver-definitions": "~2.22.0",
76
76
  "debug": "^4.3.4",
77
77
  "uuid": "^9.0.0"
78
78
  },
79
79
  "devDependencies": {
80
80
  "@arethetypeswrong/cli": "^0.17.1",
81
81
  "@biomejs/biome": "~1.9.3",
82
- "@fluid-internal/mocha-test-setup": "~2.20.0",
82
+ "@fluid-internal/mocha-test-setup": "~2.22.0",
83
83
  "@fluid-tools/build-cli": "^0.51.0",
84
84
  "@fluidframework/build-common": "^2.0.3",
85
85
  "@fluidframework/build-tools": "^0.51.0",
86
- "@fluidframework/eslint-config-fluid": "^5.6.0",
87
- "@fluidframework/telemetry-utils-previous": "npm:@fluidframework/telemetry-utils@2.13.0",
86
+ "@fluidframework/eslint-config-fluid": "^5.7.3",
87
+ "@fluidframework/telemetry-utils-previous": "npm:@fluidframework/telemetry-utils@2.21.0",
88
88
  "@microsoft/api-extractor": "7.47.8",
89
89
  "@types/debug": "^4.1.5",
90
90
  "@types/mocha": "^10.0.10",
@@ -105,11 +105,7 @@
105
105
  "typescript": "~5.4.5"
106
106
  },
107
107
  "typeValidation": {
108
- "broken": {
109
- "Class_MockLogger": {
110
- "forwardCompat": false
111
- }
112
- },
108
+ "broken": {},
113
109
  "entrypoint": "legacy"
114
110
  },
115
111
  "scripts": {
@@ -127,7 +123,7 @@
127
123
  "build:test": "npm run build:test:esm && npm run build:test:cjs",
128
124
  "build:test:cjs": "fluid-tsc commonjs --project ./src/test/tsconfig.cjs.json",
129
125
  "build:test:esm": "tsc --project ./src/test/tsconfig.json",
130
- "bump-version": "npm version minor --no-push --no-git-tag-version && npm run build:genver",
126
+ "bump-version": "npm version minor --no-push --no-git-tag-version",
131
127
  "check:are-the-types-wrong": "attw --pack .",
132
128
  "check:biome": "biome check .",
133
129
  "check:exports": "concurrently \"npm:check:exports:*\"",
@@ -142,7 +138,7 @@
142
138
  "ci:build:api-reports:current": "api-extractor run --config api-extractor/api-extractor.current.json",
143
139
  "ci:build:api-reports:legacy": "api-extractor run --config api-extractor/api-extractor.legacy.json",
144
140
  "ci:build:docs": "api-extractor run",
145
- "clean": "rimraf --glob dist lib \"*.d.ts\" \"**/*.tsbuildinfo\" \"**/*.build.log\" _api-extractor-temp nyc",
141
+ "clean": "rimraf --glob dist lib {alpha,beta,internal,legacy}.d.ts \"**/*.tsbuildinfo\" \"**/*.build.log\" _api-extractor-temp nyc",
146
142
  "eslint": "eslint --format stylish src",
147
143
  "eslint:fix": "eslint --format stylish src --fix --fix-type problem,suggestion,layout",
148
144
  "format": "npm run format:biome",
@@ -153,8 +149,8 @@
153
149
  "test": "npm run test:mocha",
154
150
  "test:coverage": "c8 npm test",
155
151
  "test:mocha": "npm run test:mocha:esm && echo skipping cjs to avoid overhead - npm run test:mocha:cjs",
156
- "test:mocha:cjs": "mocha --recursive \"dist/test/**/*.spec.*js\" --exit",
157
- "test:mocha:esm": "mocha --recursive \"lib/test/**/*.spec.*js\" --exit",
152
+ "test:mocha:cjs": "mocha --recursive \"dist/test/**/*.spec.*js\"",
153
+ "test:mocha:esm": "mocha --recursive \"lib/test/**/*.spec.*js\"",
158
154
  "test:mocha:verbose": "cross-env FLUID_TEST_VERBOSE=1 npm run test:mocha",
159
155
  "tsc": "fluid-tsc commonjs --project ./tsconfig.cjs.json && copyfiles -f ../../../common/build/build-common/src/cjs/package.json ./dist",
160
156
  "typetests:gen": "flub generate typetests --dir . -v",
package/src/logger.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  * Licensed under the MIT License.
4
4
  */
5
5
 
6
- import { performance } from "@fluid-internal/client-utils";
6
+ import { performanceNow } from "@fluid-internal/client-utils";
7
7
  import {
8
8
  type ITelemetryBaseEvent,
9
9
  type ITelemetryBaseLogger,
@@ -722,11 +722,11 @@ export class PerformanceEvent {
722
722
  }
723
723
 
724
724
  public get duration(): number {
725
- return performance.now() - this.startTime;
725
+ return performanceNow() - this.startTime;
726
726
  }
727
727
 
728
728
  private event?: ITelemetryGenericEventExt;
729
- private readonly startTime = performance.now();
729
+ private readonly startTime = performanceNow();
730
730
  private startMark?: string;
731
731
 
732
732
  protected constructor(
package/src/mockLogger.ts CHANGED
@@ -264,9 +264,11 @@ ${JSON.stringify(actualEvents)}`);
264
264
  ): number {
265
265
  let iExpectedEvent = 0;
266
266
  for (const event of this._events) {
267
+ const expectedEvent = expectedEvents[iExpectedEvent];
267
268
  if (
268
269
  iExpectedEvent < expectedEvents.length &&
269
- MockLogger.eventsMatch(event, expectedEvents[iExpectedEvent], inlineDetailsProp)
270
+ expectedEvent !== undefined &&
271
+ MockLogger.eventsMatch(event, expectedEvent, inlineDetailsProp)
270
272
  ) {
271
273
  // We found the next expected event; increment
272
274
  ++iExpectedEvent;
@@ -3,7 +3,7 @@
3
3
  * Licensed under the MIT License.
4
4
  */
5
5
 
6
- import { performance } from "@fluid-internal/client-utils";
6
+ import { performanceNow } from "@fluid-internal/client-utils";
7
7
  import type { IDisposable, ITelemetryBaseProperties } from "@fluidframework/core-interfaces";
8
8
  import { assert } from "@fluidframework/core-utils/internal";
9
9
 
@@ -192,9 +192,9 @@ export class SampledTelemetryHelper<
192
192
  codeToMeasure: () => MeasureReturnType<TMeasureReturn, TCustomMetrics>,
193
193
  bucket: string = "",
194
194
  ): MeasureReturnType<TMeasureReturn, TCustomMetrics> {
195
- const start = performance.now();
195
+ const start = performanceNow();
196
196
  const returnValue = codeToMeasure();
197
- const duration = performance.now() - start;
197
+ const duration = performanceNow() - start;
198
198
 
199
199
  let loggerData = this.measurementsMap.get(bucket);
200
200
  if (loggerData === undefined) {
package/tsconfig.json CHANGED
@@ -5,7 +5,6 @@
5
5
  "rootDir": "./src",
6
6
  "outDir": "./lib",
7
7
  "exactOptionalPropertyTypes": false,
8
- "noUncheckedIndexedAccess": false,
9
8
  },
10
9
  "include": ["src/**/*"],
11
10
  }