@fluidframework/telemetry-utils 2.0.0-dev.6.4.0.192049 → 2.0.0-dev.7.2.0.204906

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.
Files changed (72) hide show
  1. package/CHANGELOG.md +77 -0
  2. package/api-extractor.json +9 -1
  3. package/api-report/telemetry-utils.api.md +444 -0
  4. package/dist/config.d.ts +1 -1
  5. package/dist/config.d.ts.map +1 -1
  6. package/dist/config.js +4 -2
  7. package/dist/config.js.map +1 -1
  8. package/dist/error.d.ts +8 -0
  9. package/dist/error.d.ts.map +1 -1
  10. package/dist/error.js +10 -1
  11. package/dist/error.js.map +1 -1
  12. package/dist/errorLogging.d.ts +26 -0
  13. package/dist/errorLogging.d.ts.map +1 -1
  14. package/dist/errorLogging.js +43 -15
  15. package/dist/errorLogging.js.map +1 -1
  16. package/dist/events.d.ts.map +1 -1
  17. package/dist/events.js.map +1 -1
  18. package/dist/index.d.ts +1 -1
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +2 -1
  21. package/dist/index.js.map +1 -1
  22. package/dist/logger.d.ts +50 -6
  23. package/dist/logger.d.ts.map +1 -1
  24. package/dist/logger.js +108 -46
  25. package/dist/logger.js.map +1 -1
  26. package/dist/telemetry-utils-alpha.d.ts +668 -0
  27. package/dist/telemetry-utils-beta.d.ts +668 -0
  28. package/dist/telemetry-utils-public.d.ts +668 -0
  29. package/dist/telemetry-utils-untrimmed.d.ts +909 -0
  30. package/dist/telemetryTypes.d.ts +2 -2
  31. package/dist/telemetryTypes.d.ts.map +1 -1
  32. package/dist/tsdoc-metadata.json +1 -1
  33. package/dist/utils.d.ts +45 -0
  34. package/dist/utils.d.ts.map +1 -1
  35. package/dist/utils.js +50 -1
  36. package/dist/utils.js.map +1 -1
  37. package/lib/config.d.ts +1 -1
  38. package/lib/config.d.ts.map +1 -1
  39. package/lib/config.js +4 -2
  40. package/lib/config.js.map +1 -1
  41. package/lib/error.d.ts +8 -0
  42. package/lib/error.d.ts.map +1 -1
  43. package/lib/error.js +10 -1
  44. package/lib/error.js.map +1 -1
  45. package/lib/errorLogging.d.ts +26 -0
  46. package/lib/errorLogging.d.ts.map +1 -1
  47. package/lib/errorLogging.js +43 -15
  48. package/lib/errorLogging.js.map +1 -1
  49. package/lib/events.d.ts.map +1 -1
  50. package/lib/events.js.map +1 -1
  51. package/lib/index.d.ts +1 -1
  52. package/lib/index.d.ts.map +1 -1
  53. package/lib/index.js +1 -1
  54. package/lib/index.js.map +1 -1
  55. package/lib/logger.d.ts +50 -6
  56. package/lib/logger.d.ts.map +1 -1
  57. package/lib/logger.js +107 -45
  58. package/lib/logger.js.map +1 -1
  59. package/lib/telemetryTypes.d.ts +2 -2
  60. package/lib/telemetryTypes.d.ts.map +1 -1
  61. package/lib/utils.d.ts +45 -0
  62. package/lib/utils.d.ts.map +1 -1
  63. package/lib/utils.js +48 -0
  64. package/lib/utils.js.map +1 -1
  65. package/package.json +18 -19
  66. package/src/config.ts +4 -2
  67. package/src/error.ts +15 -3
  68. package/src/errorLogging.ts +43 -10
  69. package/src/events.ts +2 -0
  70. package/src/index.ts +1 -1
  71. package/src/logger.ts +94 -22
  72. package/src/utils.ts +85 -0
package/lib/utils.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"utils.js","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAUA;;;;;;GAMG;AACH,MAAM,UAAU,UAAU,CACzB,SAAkB,EAClB,MAA4B,EAC5B,KAAsC;IAEtC,IAAI,SAAS,EAAE;QACd,OAAO,IAAI,CAAC;KACZ;IACD,MAAM,QAAQ,GACb,OAAO,KAAK,KAAK,QAAQ;QACxB,CAAC,CAAC,EAAE,SAAS,EAAE,KAAK,EAAE,QAAQ,EAAE,OAAO,EAAE;QACzC,CAAC,CAAC,EAAE,QAAQ,EAAE,OAAO,EAAE,GAAG,KAAK,EAAE,CAAC;IACpC,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IACtB,OAAO,KAAK,CAAC;AACd,CAAC","sourcesContent":["/*!\n * Copyright (c) Microsoft Corporation and contributors. All rights reserved.\n * Licensed under the MIT License.\n */\nimport {\n\tITelemetryBaseEvent,\n\tITelemetryBaseLogger,\n\tITelemetryGenericEvent,\n} from \"@fluidframework/core-interfaces\";\n\n/**\n * Like assert, but logs only if the condition is false, rather than throwing\n * @param condition - The condition to attest too\n * @param logger - The logger to log with\n * @param event - The string or event to log\n * @returns The outcome of the condition\n */\nexport function logIfFalse(\n\tcondition: unknown,\n\tlogger: ITelemetryBaseLogger,\n\tevent: string | ITelemetryGenericEvent,\n): condition is true {\n\tif (condition) {\n\t\treturn true;\n\t}\n\tconst newEvent: ITelemetryBaseEvent =\n\t\ttypeof event === \"string\"\n\t\t\t? { eventName: event, category: \"error\" }\n\t\t\t: { category: \"error\", ...event };\n\tlogger.send(newEvent);\n\treturn false;\n}\n"]}
1
+ {"version":3,"file":"utils.js","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AASA,OAAO,EAAE,yBAAyB,EAAE,MAAM,UAAU,CAAC;AAGrD;;;;;;GAMG;AACH,MAAM,UAAU,UAAU,CACzB,SAAkB,EAClB,MAA4B,EAC5B,KAAsC;IAEtC,IAAI,SAAS,EAAE;QACd,OAAO,IAAI,CAAC;KACZ;IACD,MAAM,QAAQ,GACb,OAAO,KAAK,KAAK,QAAQ;QACxB,CAAC,CAAC,EAAE,SAAS,EAAE,KAAK,EAAE,QAAQ,EAAE,OAAO,EAAE;QACzC,CAAC,CAAC,EAAE,QAAQ,EAAE,OAAO,EAAE,GAAG,KAAK,EAAE,CAAC;IACpC,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IACtB,OAAO,KAAK,CAAC;AACd,CAAC;AA+BD;;;;;;;;;;;;;;;GAeG;AACH,MAAM,UAAU,mBAAmB,CAClC,MAA2B,EAC3B,YAA4B;IAE5B,MAAM,iBAAiB,GAAG,yBAAyB,CAAC,MAAM,CAAC,CAAC;IAC5D,MAAM,kBAAkB,GACvB,iBAAiB,CAAC,MAAM,CAAC,UAAU,CAAC,iCAAiC,CAAC,IAAI,KAAK,CAAC;IAEjF,MAAM,aAAa,GAAG;QACrB,IAAI,EAAE,CAAC,KAA0B,EAAQ,EAAE;YAC1C,2DAA2D;YAC3D,gIAAgI;YAChI,2LAA2L;YAC3L,IAAI,kBAAkB,IAAI,YAAY,KAAK,SAAS,IAAI,YAAY,CAAC,MAAM,EAAE,EAAE;gBAC9E,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;aACnB;QACF,CAAC;QACD,kBAAkB,EAAE,CAAC,KAAgC,EAAQ,EAAE;YAC9D,IAAI,kBAAkB,IAAI,YAAY,KAAK,SAAS,IAAI,YAAY,CAAC,MAAM,EAAE,EAAE;gBAC9E,MAAM,CAAC,kBAAkB,CAAC,KAAK,CAAC,CAAC;aACjC;QACF,CAAC;QACD,cAAc,EAAE,CAAC,KAAgC,EAAQ,EAAE;YAC1D,IAAI,kBAAkB,IAAI,YAAY,KAAK,SAAS,IAAI,YAAY,CAAC,MAAM,EAAE,EAAE;gBAC9E,MAAM,CAAC,cAAc,CAAC,KAAK,CAAC,CAAC;aAC7B;QACF,CAAC;QACD,oBAAoB,EAAE,CAAC,KAAgC,EAAQ,EAAE;YAChE,IAAI,kBAAkB,IAAI,YAAY,KAAK,SAAS,IAAI,YAAY,CAAC,MAAM,EAAE,EAAE;gBAC9E,MAAM,CAAC,oBAAoB,CAAC,KAAK,CAAC,CAAC;aACnC;QACF,CAAC;QACD,kBAAkB;KAClB,CAAC;IAEF,OAAO,aAAa,CAAC;AACtB,CAAC","sourcesContent":["/*!\n * Copyright (c) Microsoft Corporation and contributors. All rights reserved.\n * Licensed under the MIT License.\n */\nimport {\n\tITelemetryBaseEvent,\n\tITelemetryBaseLogger,\n\tITelemetryGenericEvent,\n} from \"@fluidframework/core-interfaces\";\nimport { loggerToMonitoringContext } from \"./config\";\nimport { ITelemetryGenericEventExt, ITelemetryLoggerExt } from \"./telemetryTypes\";\n\n/**\n * Like assert, but logs only if the condition is false, rather than throwing\n * @param condition - The condition to attest too\n * @param logger - The logger to log with\n * @param event - The string or event to log\n * @returns The outcome of the condition\n */\nexport function logIfFalse(\n\tcondition: unknown,\n\tlogger: ITelemetryBaseLogger,\n\tevent: string | ITelemetryGenericEvent,\n): condition is true {\n\tif (condition) {\n\t\treturn true;\n\t}\n\tconst newEvent: ITelemetryBaseEvent =\n\t\ttypeof event === \"string\"\n\t\t\t? { eventName: event, category: \"error\" }\n\t\t\t: { category: \"error\", ...event };\n\tlogger.send(newEvent);\n\treturn false;\n}\n\n/**\n * An object that contains a callback used in conjunction with the {@link createSampledLogger} utility function to provide custom logic for sampling events.\n *\n * @internal\n */\nexport interface IEventSampler {\n\t/**\n\t * @returns true if the event should be sampled or false if not\n\t */\n\tsample: () => boolean | undefined;\n}\n\n/**\n * A telemetry logger that has sampling capabilities\n *\n * @internal\n */\nexport interface ISampledTelemetryLogger extends ITelemetryLoggerExt {\n\t/**\n\t * Indicates if the feature flag to disable sampling is set.\n\t *\n\t * @remarks Exposed to enable some advanced scenarios where the code using the sampled logger\n\t * could take advantage of skipping the execution of some logic when it can determine\n\t * it won't be necessary because the telemetry event that needs it wouldn't be\n\t * emitted anyway.\n\t */\n\tisSamplingDisabled: boolean;\n}\n\n/**\n * Wraps around an existing logger matching the {@link ITelemetryLoggerExt} interface and provides the ability to only log a subset of events using a sampling strategy provided by an ${@link IEventSampler}.\n * You can chose to not provide an event sampler which is effectively a no-op, meaning that it will be treated as if the sampler always returns true.\n *\n * @remarks\n * The sampling functionality uses the Fluid telemetry logging configuration along with the optionally provided event sampling callback to determine whether an event should\n * be logged or not.\n *\n * Configuration object parameters:\n * 'Fluid.Telemetry.DisableSampling': if this config value is set to true, all events will be unsampled and therefore logged.\n * Otherwise only a sample will be logged according to the provided event sampler callback.\n *\n * Note that the same sampler is used for all APIs of the returned logger. If you want separate events flowing through the returned logger to be sampled separately, the {@link IEventSampler} you provide should track them separately.\n *\n * @internal\n */\nexport function createSampledLogger(\n\tlogger: ITelemetryLoggerExt,\n\teventSampler?: IEventSampler,\n): ISampledTelemetryLogger {\n\tconst monitoringContext = loggerToMonitoringContext(logger);\n\tconst isSamplingDisabled =\n\t\tmonitoringContext.config.getBoolean(\"Fluid.Telemetry.DisableSampling\") ?? false;\n\n\tconst sampledLogger = {\n\t\tsend: (event: ITelemetryBaseEvent): void => {\n\t\t\t// The sampler uses the following logic for sending events:\n\t\t\t// 1. If isSamplingDisabled is true, then this means events should be unsampled. Therefore we send the event without any checks.\n\t\t\t// 2. If isSamplingDisabled is false, then event should be sampled using the event sampler, if the sampler is not defined just send all events, other use the eventSampler.sample() method.\n\t\t\tif (isSamplingDisabled || eventSampler === undefined || eventSampler.sample()) {\n\t\t\t\tlogger.send(event);\n\t\t\t}\n\t\t},\n\t\tsendTelemetryEvent: (event: ITelemetryGenericEventExt): void => {\n\t\t\tif (isSamplingDisabled || eventSampler === undefined || eventSampler.sample()) {\n\t\t\t\tlogger.sendTelemetryEvent(event);\n\t\t\t}\n\t\t},\n\t\tsendErrorEvent: (event: ITelemetryGenericEventExt): void => {\n\t\t\tif (isSamplingDisabled || eventSampler === undefined || eventSampler.sample()) {\n\t\t\t\tlogger.sendErrorEvent(event);\n\t\t\t}\n\t\t},\n\t\tsendPerformanceEvent: (event: ITelemetryGenericEventExt): void => {\n\t\t\tif (isSamplingDisabled || eventSampler === undefined || eventSampler.sample()) {\n\t\t\t\tlogger.sendPerformanceEvent(event);\n\t\t\t}\n\t\t},\n\t\tisSamplingDisabled,\n\t};\n\n\treturn sampledLogger;\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fluidframework/telemetry-utils",
3
- "version": "2.0.0-dev.6.4.0.192049",
3
+ "version": "2.0.0-dev.7.2.0.204906",
4
4
  "description": "Collection of telemetry relates utilities for Fluid",
5
5
  "homepage": "https://fluidframework.com",
6
6
  "repository": {
@@ -39,39 +39,38 @@
39
39
  "temp-directory": "nyc/.nyc_output"
40
40
  },
41
41
  "dependencies": {
42
- "@fluid-internal/client-utils": "2.0.0-dev.6.4.0.192049",
43
- "@fluidframework/core-interfaces": "2.0.0-dev.6.4.0.192049",
44
- "@fluidframework/core-utils": "2.0.0-dev.6.4.0.192049",
45
- "@fluidframework/protocol-definitions": "^1.1.0",
46
- "debug": "^4.1.1",
42
+ "@fluid-internal/client-utils": "2.0.0-dev.7.2.0.204906",
43
+ "@fluidframework/core-interfaces": "2.0.0-dev.7.2.0.204906",
44
+ "@fluidframework/core-utils": "2.0.0-dev.7.2.0.204906",
45
+ "@fluidframework/protocol-definitions": "^3.0.0",
46
+ "debug": "^4.3.4",
47
47
  "events": "^3.1.0",
48
48
  "uuid": "^9.0.0"
49
49
  },
50
50
  "devDependencies": {
51
- "@fluid-tools/build-cli": "^0.22.0",
52
- "@fluidframework/build-common": "^2.0.0",
53
- "@fluidframework/build-tools": "^0.22.0",
54
- "@fluidframework/eslint-config-fluid": "^2.1.0",
55
- "@fluidframework/mocha-test-setup": "2.0.0-dev.6.4.0.192049",
56
- "@fluidframework/telemetry-utils-previous": "npm:@fluidframework/telemetry-utils@2.0.0-internal.6.3.0",
57
- "@microsoft/api-extractor": "^7.34.4",
51
+ "@fluid-tools/build-cli": "0.26.0-203096",
52
+ "@fluidframework/build-common": "^2.0.2",
53
+ "@fluidframework/build-tools": "0.26.0-203096",
54
+ "@fluidframework/eslint-config-fluid": "^3.0.0",
55
+ "@fluidframework/mocha-test-setup": "2.0.0-dev.7.2.0.204906",
56
+ "@fluidframework/telemetry-utils-previous": "npm:@fluidframework/telemetry-utils@2.0.0-internal.7.1.0",
57
+ "@microsoft/api-extractor": "^7.37.0",
58
58
  "@types/debug": "^4.1.5",
59
59
  "@types/events": "^3.0.0",
60
60
  "@types/mocha": "^9.1.1",
61
61
  "@types/node": "^16.18.38",
62
62
  "@types/uuid": "^9.0.2",
63
63
  "c8": "^7.7.1",
64
- "copyfiles": "^2.4.1",
65
64
  "cross-env": "^7.0.3",
66
- "eslint": "~8.6.0",
65
+ "eslint": "~8.50.0",
67
66
  "mocha": "^10.2.0",
68
67
  "mocha-json-output-reporter": "^2.0.1",
69
68
  "mocha-multi-reporters": "^1.5.1",
70
69
  "moment": "^2.21.0",
71
- "prettier": "~2.6.2",
70
+ "prettier": "~3.0.3",
72
71
  "rimraf": "^4.4.0",
73
72
  "sinon": "^7.4.2",
74
- "typescript": "~4.5.5"
73
+ "typescript": "~5.1.6"
75
74
  },
76
75
  "typeValidation": {
77
76
  "broken": {}
@@ -80,11 +79,11 @@
80
79
  "build": "fluid-build . --task build",
81
80
  "build:commonjs": "fluid-build . --task commonjs",
82
81
  "build:compile": "fluid-build . --task compile",
83
- "build:docs": "api-extractor run --local --typescript-compiler-folder ../../../node_modules/typescript && copyfiles -u 1 ./_api-extractor-temp/doc-models/* ../../../_api-extractor-temp/",
82
+ "build:docs": "api-extractor run --local",
84
83
  "build:esnext": "tsc --project ./tsconfig.esnext.json",
85
84
  "build:test": "tsc --project ./src/test/tsconfig.json",
86
85
  "bump-version": "npm version minor --no-push --no-git-tag-version && npm run build:genver",
87
- "ci:build:docs": "api-extractor run --typescript-compiler-folder ../../../node_modules/typescript && copyfiles -u 1 ./_api-extractor-temp/* ../../../_api-extractor-temp/",
86
+ "ci:build:docs": "api-extractor run",
88
87
  "clean": "rimraf --glob 'dist' 'lib' '*.tsbuildinfo' '*.build.log' '_api-extractor-temp' 'nyc'",
89
88
  "eslint": "eslint --format stylish src",
90
89
  "eslint:fix": "eslint --format stylish src --fix --fix-type problem,suggestion,layout",
package/src/config.ts CHANGED
@@ -77,10 +77,12 @@ function isPrimitiveType(type: string): type is PrimitiveTypeStrings {
77
77
  switch (type) {
78
78
  case "boolean":
79
79
  case "number":
80
- case "string":
80
+ case "string": {
81
81
  return true;
82
- default:
82
+ }
83
+ default: {
83
84
  return false;
85
+ }
84
86
  }
85
87
  }
86
88
 
package/src/error.ts CHANGED
@@ -23,6 +23,8 @@ import { IFluidErrorBase } from "./fluidErrorBase";
23
23
 
24
24
  /**
25
25
  * Generic wrapper for an unrecognized/uncategorized error object
26
+ *
27
+ * @internal
26
28
  */
27
29
  export class GenericError extends LoggingError implements IGenericError, IFluidErrorBase {
28
30
  readonly errorType = FluidErrorTypes.genericError;
@@ -33,9 +35,13 @@ export class GenericError extends LoggingError implements IGenericError, IFluidE
33
35
  * @param error - inner error object
34
36
  * @param props - Telemetry props to include when the error is logged
35
37
  */
36
- // TODO: Use `unknown` instead (API breaking change because error is not just an input parameter, but a public member of the class)
37
- // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any
38
- constructor(message: string, public readonly error?: any, props?: ITelemetryBaseProperties) {
38
+ constructor(
39
+ message: string,
40
+ // TODO: Use `unknown` instead (API breaking change because error is not just an input parameter, but a public member of the class)
41
+ // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any
42
+ public readonly error?: any,
43
+ props?: ITelemetryBaseProperties,
44
+ ) {
39
45
  // Don't try to log the inner error
40
46
  super(message, props, new Set(["error"]));
41
47
  }
@@ -43,6 +49,8 @@ export class GenericError extends LoggingError implements IGenericError, IFluidE
43
49
 
44
50
  /**
45
51
  * Error indicating an API is being used improperly resulting in an invalid operation.
52
+ *
53
+ * @internal
46
54
  */
47
55
  export class UsageError extends LoggingError implements IUsageError, IFluidErrorBase {
48
56
  readonly errorType = FluidErrorTypes.usageError;
@@ -55,6 +63,8 @@ export class UsageError extends LoggingError implements IUsageError, IFluidError
55
63
  /**
56
64
  * DataCorruptionError indicates that we encountered definitive evidence that the data at rest
57
65
  * backing this container is corrupted, and this container would never be expected to load properly again
66
+ *
67
+ * @internal
58
68
  */
59
69
  export class DataCorruptionError extends LoggingError implements IErrorBase, IFluidErrorBase {
60
70
  readonly errorType = FluidErrorTypes.dataCorruptionError;
@@ -73,6 +83,8 @@ export class DataCorruptionError extends LoggingError implements IErrorBase, IFl
73
83
  * The error will often originate in the dataStore or DDS implementation that is responding to incoming changes.
74
84
  * This differs from {@link DataCorruptionError} in that this may be a transient error that will not repro in another
75
85
  * client or session.
86
+ *
87
+ * @internal
76
88
  */
77
89
  export class DataProcessingError extends LoggingError implements IErrorBase, IFluidErrorBase {
78
90
  /**
@@ -27,6 +27,8 @@ const isRegularObject = (value: unknown): boolean => {
27
27
 
28
28
  /**
29
29
  * Inspect the given error for common "safe" props and return them.
30
+ *
31
+ * @internal
30
32
  */
31
33
  export function extractLogSafeErrorProperties(
32
34
  error: unknown,
@@ -95,6 +97,8 @@ function copyProps(
95
97
 
96
98
  /**
97
99
  * Metadata to annotate an error object when annotating or normalizing it
100
+ *
101
+ * @internal
98
102
  */
99
103
  export interface IFluidErrorAnnotations {
100
104
  /**
@@ -121,6 +125,8 @@ function patchLegacyError(
121
125
  * @returns A valid Fluid Error with any provided annotations applied
122
126
  * @param error - The error to normalize
123
127
  * @param annotations - Annotations to apply to the normalized error
128
+ *
129
+ * @internal
124
130
  */
125
131
  export function normalizeError(
126
132
  error: unknown,
@@ -190,6 +196,8 @@ let stackPopulatedOnCreation: boolean | undefined;
190
196
  * For such cases it's better to not read stack property right away, but rather delay it until / if it's needed
191
197
  * Some browsers will populate stack right away, others require throwing Error, so we do auto-detection on the fly.
192
198
  * @returns Error object that has stack populated.
199
+ *
200
+ * @internal
193
201
  */
194
202
  export function generateErrorWithStack(): Error {
195
203
  const err = new Error("<<generated stack>>");
@@ -209,6 +217,12 @@ export function generateErrorWithStack(): Error {
209
217
  }
210
218
  }
211
219
 
220
+ /**
221
+ * Generate a stack at this callsite as if an error were thrown from here.
222
+ * @returns the callstack (does not throw)
223
+ *
224
+ * @internal
225
+ */
212
226
  export function generateStack(): string | undefined {
213
227
  return generateErrorWithStack().stack;
214
228
  }
@@ -219,6 +233,8 @@ export function generateStack(): string | undefined {
219
233
  * @param innerError - An error from untrusted/unknown origins
220
234
  * @param newErrorFn - callback that will create a new error given the original error's message
221
235
  * @returns A new error object "wrapping" the given error
236
+ *
237
+ * @internal
222
238
  */
223
239
  export function wrapError<T extends LoggingError>(
224
240
  innerError: unknown,
@@ -258,6 +274,8 @@ export function wrapError<T extends LoggingError>(
258
274
  * The same as wrapError, but also logs the innerError, including the wrapping error's instance ID.
259
275
  *
260
276
  * @typeParam T - The kind of wrapper error to create.
277
+ *
278
+ * @internal
261
279
  */
262
280
  export function wrapErrorAndLog<T extends LoggingError>(
263
281
  innerError: unknown,
@@ -304,6 +322,8 @@ export function overwriteStack(error: IFluidErrorBase | LoggingError, stack: str
304
322
  * True for any error object that is an (optionally normalized) external error
305
323
  * False for any error we created and raised within the FF codebase via LoggingError base class,
306
324
  * or wrapped in a well-known error type
325
+ *
326
+ * @internal
307
327
  */
308
328
  export function isExternalError(error: unknown): boolean {
309
329
  // LoggingErrors are an internal FF error type. However, an external error can be converted
@@ -351,12 +371,15 @@ function isTelemetryEventPropertyValue(x: unknown): x is TelemetryBaseEventPrope
351
371
  case "string":
352
372
  case "number":
353
373
  case "boolean":
354
- case "undefined":
374
+ case "undefined": {
355
375
  return true;
356
- default:
376
+ }
377
+ default: {
357
378
  return false;
379
+ }
358
380
  }
359
381
  }
382
+
360
383
  /**
361
384
  * Walk an object's enumerable properties to find those fit for telemetry.
362
385
  */
@@ -371,14 +394,12 @@ function getValidTelemetryProps(obj: object, keysToOmit: Set<string>): ITelemetr
371
394
  | Tagged<TelemetryEventPropertyTypeExt>;
372
395
 
373
396
  // ensure only valid props get logged, since props of logging error could be in any shape
374
- if (isTaggedTelemetryPropertyValue(val)) {
375
- props[key] = {
376
- value: filterValidTelemetryProps(val.value, key),
377
- tag: val.tag,
378
- };
379
- } else {
380
- props[key] = filterValidTelemetryProps(val, key);
381
- }
397
+ props[key] = isTaggedTelemetryPropertyValue(val)
398
+ ? {
399
+ value: filterValidTelemetryProps(val.value, key),
400
+ tag: val.tag,
401
+ }
402
+ : filterValidTelemetryProps(val, key);
382
403
  }
383
404
  return props;
384
405
  }
@@ -389,6 +410,8 @@ function getValidTelemetryProps(obj: object, keysToOmit: Set<string>): ITelemetr
389
410
  * Avoids runtime errors with circular references.
390
411
  * Not ideal, as will cut values that are not necessarily circular references.
391
412
  * Could be improved by implementing Node's util.inspect() for browser (minus all the coloring code)
413
+ *
414
+ * @internal
392
415
  */
393
416
  // TODO: Use `unknown` instead (API breaking change)
394
417
  /* eslint-disable @typescript-eslint/no-explicit-any */
@@ -412,6 +435,8 @@ export const getCircularReplacer = (): ((key: string, value: unknown) => any) =>
412
435
  * will be logged in accordance with their tag, if present.
413
436
  *
414
437
  * PLEASE take care to avoid setting sensitive data on this object without proper tagging!
438
+ *
439
+ * @internal
415
440
  */
416
441
  export class LoggingError
417
442
  extends Error
@@ -494,8 +519,16 @@ export class LoggingError
494
519
 
495
520
  /**
496
521
  * The Error class used when normalizing an external error
522
+ *
523
+ * @internal
497
524
  */
498
525
  export const NORMALIZED_ERROR_TYPE = "genericError";
526
+
527
+ /**
528
+ * Subclass of LoggingError returned by normalizeError
529
+ *
530
+ * @internal
531
+ */
499
532
  class NormalizedLoggingError extends LoggingError {
500
533
  // errorType "genericError" is used as a default value throughout the code.
501
534
  // Note that this matches ContainerErrorType/DriverErrorType's genericError
package/src/events.ts CHANGED
@@ -3,6 +3,8 @@
3
3
  * Licensed under the MIT License.
4
4
  */
5
5
 
6
+ // False positive: this is an import from the `events` package, not from Node.
7
+ // eslint-disable-next-line unicorn/prefer-node-protocol
6
8
  import { EventEmitter } from "events";
7
9
  import { ITelemetryLoggerExt } from "./telemetryTypes";
8
10
 
package/src/index.ts CHANGED
@@ -68,7 +68,7 @@ export {
68
68
  export { MockLogger } from "./mockLogger";
69
69
  export { ThresholdCounter } from "./thresholdCounter";
70
70
  export { SampledTelemetryHelper } from "./sampledTelemetryHelper";
71
- export { logIfFalse } from "./utils";
71
+ export { logIfFalse, createSampledLogger, IEventSampler, ISampledTelemetryLogger } from "./utils";
72
72
  export {
73
73
  TelemetryEventPropertyTypeExt,
74
74
  ITelemetryEventExt,
package/src/logger.ts CHANGED
@@ -305,26 +305,30 @@ export class TaggedLoggerAdapter implements ITelemetryBaseLogger {
305
305
  ? taggableProp
306
306
  : { value: taggableProp, tag: undefined };
307
307
  switch (tag) {
308
- case undefined:
308
+ case undefined: {
309
309
  // No tag means we can log plainly
310
310
  newEvent[key] = value;
311
311
  break;
312
+ }
312
313
  case "PackageData": // For back-compat
313
- case TelemetryDataTag.CodeArtifact:
314
+ case TelemetryDataTag.CodeArtifact: {
314
315
  // For Microsoft applications, CodeArtifact is safe for now
315
316
  // (we don't load 3P code in 1P apps)
316
317
  newEvent[key] = value;
317
318
  break;
318
- case TelemetryDataTag.UserData:
319
+ }
320
+ case TelemetryDataTag.UserData: {
319
321
  // Strip out anything tagged explicitly as UserData.
320
322
  // Alternate strategy would be to hash these props
321
323
  newEvent[key] = "REDACTED (UserData)";
322
324
  break;
323
- default:
325
+ }
326
+ default: {
324
327
  // If we encounter a tag we don't recognize
325
328
  // then we must assume we should scrub.
326
329
  newEvent[key] = "REDACTED (unknown tag)";
327
330
  break;
331
+ }
328
332
  }
329
333
  }
330
334
  this.logger.send(newEvent);
@@ -404,11 +408,7 @@ export class ChildLogger extends TelemetryLogger {
404
408
  return child;
405
409
  }
406
410
 
407
- return new ChildLogger(
408
- baseLogger ? baseLogger : { send(): void {} },
409
- namespace,
410
- properties,
411
- );
411
+ return new ChildLogger(baseLogger ?? { send(): void {} }, namespace, properties);
412
412
  }
413
413
 
414
414
  private constructor(
@@ -489,7 +489,7 @@ export class MultiSinkLogger extends TelemetryLogger {
489
489
  loggers: ITelemetryBaseLogger[] = [],
490
490
  tryInheritProperties?: true,
491
491
  ) {
492
- let realProperties = properties !== undefined ? { ...properties } : undefined;
492
+ let realProperties = properties === undefined ? undefined : { ...properties };
493
493
  if (tryInheritProperties === true) {
494
494
  const merge = (realProperties ??= {});
495
495
  loggers
@@ -566,22 +566,55 @@ export interface IPerformanceEventMarkers {
566
566
  * Helper class to log performance events
567
567
  */
568
568
  export class PerformanceEvent {
569
+ /**
570
+ * Creates an instance of {@link PerformanceEvent} and starts measurements
571
+ * @param logger - the logger to be used for publishing events
572
+ * @param event - the logging event details which will be published with the performance measurements
573
+ * @param markers - See {@link IPerformanceEventMarkers}
574
+ * @param recordHeapSize - whether or not to also record memory performance
575
+ * @param emitLogs - should this instance emit logs. If set to false, logs will not be emitted to the logger,
576
+ * but measurements will still be performed and any specified markers will be generated.
577
+ * @returns An instance of {@link PerformanceEvent}
578
+ */
569
579
  public static start(
570
580
  logger: ITelemetryLoggerExt,
571
581
  event: ITelemetryGenericEvent,
572
582
  markers?: IPerformanceEventMarkers,
573
583
  recordHeapSize: boolean = false,
584
+ emitLogs: boolean = true,
574
585
  ): PerformanceEvent {
575
- return new PerformanceEvent(logger, event, markers, recordHeapSize);
586
+ return new PerformanceEvent(logger, event, markers, recordHeapSize, emitLogs);
576
587
  }
577
588
 
589
+ /**
590
+ * Measure a synchronous task
591
+ * @param logger - the logger to be used for publishing events
592
+ * @param event - the logging event details which will be published with the performance measurements
593
+ * @param callback - the task to be executed and measured
594
+ * @param markers - See {@link IPerformanceEventMarkers}
595
+ * @param sampleThreshold - events with the same name and category will be sent to the logger
596
+ * only when we hit this many executions of the task. If unspecified, all events will be sent.
597
+ * @returns The results of the executed task
598
+ *
599
+ * @remarks Note that if the "same" event (category + eventName) would be emitted by different
600
+ * tasks (`callback`), `sampleThreshold` is still applied only based on the event's category + eventName,
601
+ * so executing either of the tasks will increase the internal counter and they
602
+ * effectively "share" the sampling rate for the event.
603
+ */
578
604
  public static timedExec<T>(
579
605
  logger: ITelemetryLoggerExt,
580
606
  event: ITelemetryGenericEvent,
581
607
  callback: (event: PerformanceEvent) => T,
582
608
  markers?: IPerformanceEventMarkers,
609
+ sampleThreshold: number = 1,
583
610
  ): T {
584
- const perfEvent = PerformanceEvent.start(logger, event, markers);
611
+ const perfEvent = PerformanceEvent.start(
612
+ logger,
613
+ event,
614
+ markers,
615
+ undefined, // recordHeapSize
616
+ PerformanceEvent.shouldReport(event, sampleThreshold),
617
+ );
585
618
  try {
586
619
  const ret = callback(perfEvent);
587
620
  perfEvent.autoEnd();
@@ -592,14 +625,37 @@ export class PerformanceEvent {
592
625
  }
593
626
  }
594
627
 
628
+ /**
629
+ * Measure an asynchronous task
630
+ * @param logger - the logger to be used for publishing events
631
+ * @param event - the logging event details which will be published with the performance measurements
632
+ * @param callback - the task to be executed and measured
633
+ * @param markers - See {@link IPerformanceEventMarkers}
634
+ * @param recordHeapSize - whether or not to also record memory performance
635
+ * @param sampleThreshold - events with the same name and category will be sent to the logger
636
+ * only when we hit this many executions of the task. If unspecified, all events will be sent.
637
+ * @returns The results of the executed task
638
+ *
639
+ * @remarks Note that if the "same" event (category + eventName) would be emitted by different
640
+ * tasks (`callback`), `sampleThreshold` is still applied only based on the event's category + eventName,
641
+ * so executing either of the tasks will increase the internal counter and they
642
+ * effectively "share" the sampling rate for the event.
643
+ */
595
644
  public static async timedExecAsync<T>(
596
645
  logger: ITelemetryLoggerExt,
597
646
  event: ITelemetryGenericEvent,
598
647
  callback: (event: PerformanceEvent) => Promise<T>,
599
648
  markers?: IPerformanceEventMarkers,
600
649
  recordHeapSize?: boolean,
650
+ sampleThreshold: number = 1,
601
651
  ): Promise<T> {
602
- const perfEvent = PerformanceEvent.start(logger, event, markers, recordHeapSize);
652
+ const perfEvent = PerformanceEvent.start(
653
+ logger,
654
+ event,
655
+ markers,
656
+ recordHeapSize,
657
+ PerformanceEvent.shouldReport(event, sampleThreshold),
658
+ );
603
659
  try {
604
660
  const ret = await callback(perfEvent);
605
661
  perfEvent.autoEnd();
@@ -624,14 +680,14 @@ export class PerformanceEvent {
624
680
  event: ITelemetryGenericEvent,
625
681
  private readonly markers: IPerformanceEventMarkers = { end: true, cancel: "generic" },
626
682
  private readonly recordHeapSize: boolean = false,
683
+ private readonly emitLogs: boolean = true,
627
684
  ) {
628
685
  this.event = { ...event };
629
686
  if (this.markers.start) {
630
687
  this.reportEvent("start");
631
688
  }
632
689
 
633
- // eslint-disable-next-line unicorn/no-null
634
- if (typeof window === "object" && window != null && window.performance?.mark) {
690
+ if (typeof window === "object" && window?.performance?.mark) {
635
691
  this.startMark = `${event.eventName}-start`;
636
692
  window.performance.mark(this.startMark);
637
693
  }
@@ -687,6 +743,10 @@ export class PerformanceEvent {
687
743
  return;
688
744
  }
689
745
 
746
+ if (!this.emitLogs) {
747
+ return;
748
+ }
749
+
690
750
  const event: ITelemetryPerformanceEvent = { ...this.event, ...props };
691
751
  event.eventName = `${event.eventName}_${eventNameSuffix}`;
692
752
  if (eventNameSuffix !== "start") {
@@ -702,18 +762,25 @@ export class PerformanceEvent {
702
762
  }
703
763
  }
704
764
  } else if (this.recordHeapSize) {
705
- this.startMemoryCollection = (
706
- performance as PerformanceWithMemory
707
- )?.memory?.usedJSHeapSize;
765
+ this.startMemoryCollection = (performance as PerformanceWithMemory)?.memory
766
+ ?.usedJSHeapSize;
708
767
  }
709
768
 
710
769
  this.logger.sendPerformanceEvent(event, error);
711
770
  }
771
+
772
+ private static readonly eventHits = new Map<string, number>();
773
+ private static shouldReport(event: ITelemetryGenericEvent, sampleThreshold: number): boolean {
774
+ const eventKey = `.${event.category}.${event.eventName}`;
775
+ const hitCount = PerformanceEvent.eventHits.get(eventKey) ?? 0;
776
+ PerformanceEvent.eventHits.set(eventKey, hitCount >= sampleThreshold ? 1 : hitCount + 1);
777
+ return hitCount % sampleThreshold === 0;
778
+ }
712
779
  }
713
780
 
714
781
  /**
715
782
  * Null logger that no-ops for all telemetry events passed to it.
716
- * @deprecated - This will be removed in a future release.
783
+ * @deprecated This will be removed in a future release.
717
784
  * For internal use within the FluidFramework codebase, use {@link createChildLogger} with no arguments instead.
718
785
  * For external consumers we recommend writing a trivial implementation of {@link @fluidframework/core-interfaces#ITelemetryBaseLogger}
719
786
  * where the send() method does nothing and using that.
@@ -769,17 +836,20 @@ function convertToBasePropertyTypeUntagged(
769
836
  case "string":
770
837
  case "number":
771
838
  case "boolean":
772
- case "undefined":
839
+ case "undefined": {
773
840
  return x;
774
- case "object":
841
+ }
842
+ case "object": {
775
843
  // We assume this is an array or flat object based on the input types
776
844
  return JSON.stringify(x);
777
- default:
845
+ }
846
+ default: {
778
847
  // should never reach this case based on the input types
779
848
  console.error(
780
849
  `convertToBasePropertyTypeUntagged: INVALID PROPERTY (typed as ${typeof x})`,
781
850
  );
782
851
  return `INVALID PROPERTY (typed as ${typeof x})`;
852
+ }
783
853
  }
784
854
  }
785
855
 
@@ -811,6 +881,8 @@ export const tagData = <
811
881
  // eslint-disable-next-line unicorn/no-array-reduce, unicorn/prefer-object-from-entries
812
882
  .reduce((pv, cv) => {
813
883
  const [key, value] = cv;
884
+ // The ternary form is less legible in this case.
885
+ // eslint-disable-next-line unicorn/prefer-ternary
814
886
  if (typeof value === "function") {
815
887
  // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
816
888
  pv[key] = () => {
package/src/utils.ts CHANGED
@@ -7,6 +7,8 @@ import {
7
7
  ITelemetryBaseLogger,
8
8
  ITelemetryGenericEvent,
9
9
  } from "@fluidframework/core-interfaces";
10
+ import { loggerToMonitoringContext } from "./config";
11
+ import { ITelemetryGenericEventExt, ITelemetryLoggerExt } from "./telemetryTypes";
10
12
 
11
13
  /**
12
14
  * Like assert, but logs only if the condition is false, rather than throwing
@@ -30,3 +32,86 @@ export function logIfFalse(
30
32
  logger.send(newEvent);
31
33
  return false;
32
34
  }
35
+
36
+ /**
37
+ * An object that contains a callback used in conjunction with the {@link createSampledLogger} utility function to provide custom logic for sampling events.
38
+ *
39
+ * @internal
40
+ */
41
+ export interface IEventSampler {
42
+ /**
43
+ * @returns true if the event should be sampled or false if not
44
+ */
45
+ sample: () => boolean | undefined;
46
+ }
47
+
48
+ /**
49
+ * A telemetry logger that has sampling capabilities
50
+ *
51
+ * @internal
52
+ */
53
+ export interface ISampledTelemetryLogger extends ITelemetryLoggerExt {
54
+ /**
55
+ * Indicates if the feature flag to disable sampling is set.
56
+ *
57
+ * @remarks Exposed to enable some advanced scenarios where the code using the sampled logger
58
+ * could take advantage of skipping the execution of some logic when it can determine
59
+ * it won't be necessary because the telemetry event that needs it wouldn't be
60
+ * emitted anyway.
61
+ */
62
+ isSamplingDisabled: boolean;
63
+ }
64
+
65
+ /**
66
+ * Wraps around an existing logger matching the {@link ITelemetryLoggerExt} interface and provides the ability to only log a subset of events using a sampling strategy provided by an ${@link IEventSampler}.
67
+ * You can chose to not provide an event sampler which is effectively a no-op, meaning that it will be treated as if the sampler always returns true.
68
+ *
69
+ * @remarks
70
+ * The sampling functionality uses the Fluid telemetry logging configuration along with the optionally provided event sampling callback to determine whether an event should
71
+ * be logged or not.
72
+ *
73
+ * Configuration object parameters:
74
+ * 'Fluid.Telemetry.DisableSampling': if this config value is set to true, all events will be unsampled and therefore logged.
75
+ * Otherwise only a sample will be logged according to the provided event sampler callback.
76
+ *
77
+ * Note that the same sampler is used for all APIs of the returned logger. If you want separate events flowing through the returned logger to be sampled separately, the {@link IEventSampler} you provide should track them separately.
78
+ *
79
+ * @internal
80
+ */
81
+ export function createSampledLogger(
82
+ logger: ITelemetryLoggerExt,
83
+ eventSampler?: IEventSampler,
84
+ ): ISampledTelemetryLogger {
85
+ const monitoringContext = loggerToMonitoringContext(logger);
86
+ const isSamplingDisabled =
87
+ monitoringContext.config.getBoolean("Fluid.Telemetry.DisableSampling") ?? false;
88
+
89
+ const sampledLogger = {
90
+ send: (event: ITelemetryBaseEvent): void => {
91
+ // The sampler uses the following logic for sending events:
92
+ // 1. If isSamplingDisabled is true, then this means events should be unsampled. Therefore we send the event without any checks.
93
+ // 2. If isSamplingDisabled is false, then event should be sampled using the event sampler, if the sampler is not defined just send all events, other use the eventSampler.sample() method.
94
+ if (isSamplingDisabled || eventSampler === undefined || eventSampler.sample()) {
95
+ logger.send(event);
96
+ }
97
+ },
98
+ sendTelemetryEvent: (event: ITelemetryGenericEventExt): void => {
99
+ if (isSamplingDisabled || eventSampler === undefined || eventSampler.sample()) {
100
+ logger.sendTelemetryEvent(event);
101
+ }
102
+ },
103
+ sendErrorEvent: (event: ITelemetryGenericEventExt): void => {
104
+ if (isSamplingDisabled || eventSampler === undefined || eventSampler.sample()) {
105
+ logger.sendErrorEvent(event);
106
+ }
107
+ },
108
+ sendPerformanceEvent: (event: ITelemetryGenericEventExt): void => {
109
+ if (isSamplingDisabled || eventSampler === undefined || eventSampler.sample()) {
110
+ logger.sendPerformanceEvent(event);
111
+ }
112
+ },
113
+ isSamplingDisabled,
114
+ };
115
+
116
+ return sampledLogger;
117
+ }