@fluidframework/telemetry-utils 1.4.0-121020 → 2.0.0-dev-rc.1.0.0.225277

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 (156) hide show
  1. package/.eslintrc.js +12 -13
  2. package/.mocharc.js +12 -0
  3. package/CHANGELOG.md +249 -0
  4. package/README.md +68 -1
  5. package/api-extractor-esm.json +5 -0
  6. package/api-extractor-lint.json +4 -0
  7. package/api-extractor.json +2 -2
  8. package/api-report/telemetry-utils.api.md +444 -0
  9. package/dist/config.d.ts +47 -16
  10. package/dist/config.d.ts.map +1 -1
  11. package/dist/config.js +88 -38
  12. package/dist/config.js.map +1 -1
  13. package/dist/error.d.ts +112 -0
  14. package/dist/error.d.ts.map +1 -0
  15. package/dist/error.js +159 -0
  16. package/dist/error.js.map +1 -0
  17. package/dist/errorLogging.d.ts +86 -20
  18. package/dist/errorLogging.d.ts.map +1 -1
  19. package/dist/errorLogging.js +190 -60
  20. package/dist/errorLogging.js.map +1 -1
  21. package/dist/eventEmitterWithErrorHandling.d.ts +9 -3
  22. package/dist/eventEmitterWithErrorHandling.d.ts.map +1 -1
  23. package/dist/eventEmitterWithErrorHandling.js +16 -3
  24. package/dist/eventEmitterWithErrorHandling.js.map +1 -1
  25. package/dist/events.d.ts +27 -3
  26. package/dist/events.d.ts.map +1 -1
  27. package/dist/events.js +26 -2
  28. package/dist/events.js.map +1 -1
  29. package/dist/fluidErrorBase.d.ts +57 -16
  30. package/dist/fluidErrorBase.d.ts.map +1 -1
  31. package/dist/fluidErrorBase.js +27 -14
  32. package/dist/fluidErrorBase.js.map +1 -1
  33. package/dist/index.d.ts +12 -11
  34. package/dist/index.d.ts.map +1 -1
  35. package/dist/index.js +55 -21
  36. package/dist/index.js.map +1 -1
  37. package/dist/logger.d.ts +269 -53
  38. package/dist/logger.d.ts.map +1 -1
  39. package/dist/logger.js +423 -132
  40. package/dist/logger.js.map +1 -1
  41. package/dist/mockLogger.d.ts +39 -12
  42. package/dist/mockLogger.d.ts.map +1 -1
  43. package/dist/mockLogger.js +105 -22
  44. package/dist/mockLogger.js.map +1 -1
  45. package/dist/sampledTelemetryHelper.d.ts +18 -12
  46. package/dist/sampledTelemetryHelper.d.ts.map +1 -1
  47. package/dist/sampledTelemetryHelper.js +28 -19
  48. package/dist/sampledTelemetryHelper.js.map +1 -1
  49. package/dist/telemetry-utils-alpha.d.ts +290 -0
  50. package/dist/telemetry-utils-beta.d.ts +264 -0
  51. package/dist/telemetry-utils-public.d.ts +264 -0
  52. package/dist/telemetry-utils-untrimmed.d.ts +1102 -0
  53. package/dist/telemetryTypes.d.ts +115 -0
  54. package/dist/telemetryTypes.d.ts.map +1 -0
  55. package/dist/telemetryTypes.js +7 -0
  56. package/dist/telemetryTypes.js.map +1 -0
  57. package/dist/thresholdCounter.d.ts +6 -5
  58. package/dist/thresholdCounter.d.ts.map +1 -1
  59. package/dist/thresholdCounter.js +4 -3
  60. package/dist/thresholdCounter.js.map +1 -1
  61. package/dist/tsdoc-metadata.json +11 -0
  62. package/dist/utils.d.ts +54 -3
  63. package/dist/utils.d.ts.map +1 -1
  64. package/dist/utils.js +58 -3
  65. package/dist/utils.js.map +1 -1
  66. package/lib/config.d.ts +47 -16
  67. package/lib/config.d.ts.map +1 -1
  68. package/lib/config.js +85 -36
  69. package/lib/config.js.map +1 -1
  70. package/lib/error.d.ts +112 -0
  71. package/lib/error.d.ts.map +1 -0
  72. package/lib/error.js +150 -0
  73. package/lib/error.js.map +1 -0
  74. package/lib/errorLogging.d.ts +86 -20
  75. package/lib/errorLogging.d.ts.map +1 -1
  76. package/lib/errorLogging.js +189 -60
  77. package/lib/errorLogging.js.map +1 -1
  78. package/lib/eventEmitterWithErrorHandling.d.ts +9 -3
  79. package/lib/eventEmitterWithErrorHandling.d.ts.map +1 -1
  80. package/lib/eventEmitterWithErrorHandling.js +15 -2
  81. package/lib/eventEmitterWithErrorHandling.js.map +1 -1
  82. package/lib/events.d.ts +27 -3
  83. package/lib/events.d.ts.map +1 -1
  84. package/lib/events.js +26 -2
  85. package/lib/events.js.map +1 -1
  86. package/lib/fluidErrorBase.d.ts +57 -16
  87. package/lib/fluidErrorBase.d.ts.map +1 -1
  88. package/lib/fluidErrorBase.js +27 -14
  89. package/lib/fluidErrorBase.js.map +1 -1
  90. package/lib/index.d.ts +12 -11
  91. package/lib/index.d.ts.map +1 -1
  92. package/lib/index.js +11 -11
  93. package/lib/index.js.map +1 -1
  94. package/lib/logger.d.ts +269 -53
  95. package/lib/logger.d.ts.map +1 -1
  96. package/lib/logger.js +415 -131
  97. package/lib/logger.js.map +1 -1
  98. package/lib/mockLogger.d.ts +39 -12
  99. package/lib/mockLogger.d.ts.map +1 -1
  100. package/lib/mockLogger.js +106 -23
  101. package/lib/mockLogger.js.map +1 -1
  102. package/lib/sampledTelemetryHelper.d.ts +18 -12
  103. package/lib/sampledTelemetryHelper.d.ts.map +1 -1
  104. package/lib/sampledTelemetryHelper.js +26 -17
  105. package/lib/sampledTelemetryHelper.js.map +1 -1
  106. package/lib/telemetry-utils-alpha.d.mts +290 -0
  107. package/lib/telemetry-utils-beta.d.mts +264 -0
  108. package/lib/telemetry-utils-public.d.mts +264 -0
  109. package/lib/telemetry-utils-untrimmed.d.mts +1102 -0
  110. package/lib/telemetryTypes.d.ts +115 -0
  111. package/lib/telemetryTypes.d.ts.map +1 -0
  112. package/lib/telemetryTypes.js +6 -0
  113. package/lib/telemetryTypes.js.map +1 -0
  114. package/lib/thresholdCounter.d.ts +6 -5
  115. package/lib/thresholdCounter.d.ts.map +1 -1
  116. package/lib/thresholdCounter.js +4 -3
  117. package/lib/thresholdCounter.js.map +1 -1
  118. package/lib/utils.d.ts +54 -3
  119. package/lib/utils.d.ts.map +1 -1
  120. package/lib/utils.js +56 -2
  121. package/lib/utils.js.map +1 -1
  122. package/package.json +86 -57
  123. package/prettier.config.cjs +8 -0
  124. package/src/config.ts +254 -189
  125. package/src/error.ts +235 -0
  126. package/src/errorLogging.ts +440 -290
  127. package/src/eventEmitterWithErrorHandling.ts +26 -14
  128. package/src/events.ts +54 -25
  129. package/src/fluidErrorBase.ts +94 -46
  130. package/src/index.ts +76 -17
  131. package/src/logger.ts +972 -505
  132. package/src/mockLogger.ts +225 -83
  133. package/src/sampledTelemetryHelper.ts +136 -128
  134. package/src/telemetryTypes.ts +140 -0
  135. package/src/thresholdCounter.ts +38 -37
  136. package/src/utils.ts +108 -17
  137. package/tsconfig.esnext.json +6 -6
  138. package/tsconfig.json +9 -13
  139. package/dist/debugLogger.d.ts +0 -39
  140. package/dist/debugLogger.d.ts.map +0 -1
  141. package/dist/debugLogger.js +0 -101
  142. package/dist/debugLogger.js.map +0 -1
  143. package/dist/packageVersion.d.ts +0 -9
  144. package/dist/packageVersion.d.ts.map +0 -1
  145. package/dist/packageVersion.js +0 -12
  146. package/dist/packageVersion.js.map +0 -1
  147. package/lib/debugLogger.d.ts +0 -39
  148. package/lib/debugLogger.d.ts.map +0 -1
  149. package/lib/debugLogger.js +0 -97
  150. package/lib/debugLogger.js.map +0 -1
  151. package/lib/packageVersion.d.ts +0 -9
  152. package/lib/packageVersion.d.ts.map +0 -1
  153. package/lib/packageVersion.js +0 -9
  154. package/lib/packageVersion.js.map +0 -1
  155. package/src/debugLogger.ts +0 -126
  156. package/src/packageVersion.ts +0 -9
@@ -4,88 +4,122 @@
4
4
  */
5
5
 
6
6
  import {
7
- ILoggingError,
8
- ITaggedTelemetryPropertyType,
9
- ITelemetryLogger,
10
- ITelemetryProperties,
11
- } from "@fluidframework/common-definitions";
7
+ ILoggingError,
8
+ ITelemetryBaseProperties,
9
+ TelemetryBaseEventPropertyType,
10
+ Tagged,
11
+ } from "@fluidframework/core-interfaces";
12
12
  import { v4 as uuid } from "uuid";
13
13
  import {
14
- hasErrorInstanceId,
15
- IFluidErrorBase,
16
- isFluidError,
17
- isValidLegacyError,
14
+ hasErrorInstanceId,
15
+ IFluidErrorBase,
16
+ isFluidError,
17
+ isValidLegacyError,
18
18
  } from "./fluidErrorBase";
19
+ import { ITelemetryLoggerExt, TelemetryEventPropertyTypeExt } from "./telemetryTypes";
19
20
 
20
- /** @returns true if value is an object but neither null nor an array */
21
- const isRegularObject = (value: any): boolean => {
22
- return value !== null && !Array.isArray(value) && typeof value === "object";
21
+ /**
22
+ * Determines if the provided value is an object but neither null nor an array.
23
+ */
24
+ const isRegularObject = (value: unknown): boolean => {
25
+ return value !== null && !Array.isArray(value) && typeof value === "object";
23
26
  };
24
27
 
25
- /** Inspect the given error for common "safe" props and return them */
26
- export function extractLogSafeErrorProperties(error: any, sanitizeStack: boolean) {
27
- const removeMessageFromStack = (stack: string, errorName?: string) => {
28
- if (!sanitizeStack) {
29
- return stack;
30
- }
31
- const stackFrames = stack.split("\n");
32
- stackFrames.shift(); // Remove "[ErrorName]: [ErrorMessage]"
33
- if (errorName !== undefined) {
34
- stackFrames.unshift(errorName); // Add "[ErrorName]"
35
- }
36
- return stackFrames.join("\n");
37
- };
38
-
39
- const message = (typeof error?.message === "string")
40
- ? error.message as string
41
- : String(error);
42
-
43
- const safeProps: { message: string; errorType?: string; stack?: string; } = {
44
- message,
45
- };
46
-
47
- if (isRegularObject(error)) {
48
- const { errorType, stack, name } = error;
49
-
50
- if (typeof errorType === "string") {
51
- safeProps.errorType = errorType;
52
- }
53
-
54
- if (typeof stack === "string") {
55
- const errorName = (typeof name === "string") ? name : undefined;
56
- safeProps.stack = removeMessageFromStack(stack, errorName);
57
- }
58
- }
59
-
60
- return safeProps;
28
+ /**
29
+ * Inspect the given error for common "safe" props and return them.
30
+ *
31
+ * @internal
32
+ */
33
+ export function extractLogSafeErrorProperties(
34
+ error: unknown,
35
+ sanitizeStack: boolean,
36
+ ): {
37
+ message: string;
38
+ errorType?: string | undefined;
39
+ stack?: string | undefined;
40
+ } {
41
+ const removeMessageFromStack = (stack: string, errorName?: string): string => {
42
+ if (!sanitizeStack) {
43
+ return stack;
44
+ }
45
+ const stackFrames = stack.split("\n");
46
+ stackFrames.shift(); // Remove "[ErrorName]: [ErrorMessage]"
47
+ if (errorName !== undefined) {
48
+ stackFrames.unshift(errorName); // Add "[ErrorName]"
49
+ }
50
+ return stackFrames.join("\n");
51
+ };
52
+
53
+ const message =
54
+ typeof (error as Partial<Error>)?.message === "string"
55
+ ? (error as Error).message
56
+ : String(error);
57
+
58
+ const safeProps: { message: string; errorType?: string; stack?: string } = {
59
+ message,
60
+ };
61
+
62
+ if (isRegularObject(error)) {
63
+ const { errorType, stack, name } = error as Partial<IFluidErrorBase>;
64
+
65
+ if (typeof errorType === "string") {
66
+ safeProps.errorType = errorType;
67
+ }
68
+
69
+ if (typeof stack === "string") {
70
+ const errorName = typeof name === "string" ? name : undefined;
71
+ safeProps.stack = removeMessageFromStack(stack, errorName);
72
+ }
73
+ }
74
+
75
+ return safeProps;
61
76
  }
62
77
 
63
- /** type guard for ILoggingError interface */
64
- export const isILoggingError = (x: any): x is ILoggingError => typeof x?.getTelemetryProperties === "function";
78
+ /**
79
+ * Type-guard for {@link @fluidframework/core-interfaces#ILoggingError}.
80
+ *
81
+ * @internal
82
+ */
83
+ export const isILoggingError = (x: unknown): x is ILoggingError =>
84
+ typeof (x as Partial<ILoggingError>)?.getTelemetryProperties === "function";
65
85
 
66
- /** Copy props from source onto target, but do not overwrite an existing prop that matches */
67
- function copyProps(target: ITelemetryProperties | LoggingError, source: ITelemetryProperties) {
68
- for (const key of Object.keys(source)) {
69
- if (target[key] === undefined) {
70
- target[key] = source[key];
71
- }
72
- }
86
+ /**
87
+ * Copy props from source onto target, but do not overwrite an existing prop that matches
88
+ */
89
+ function copyProps(
90
+ target: ITelemetryBaseProperties | LoggingError,
91
+ source: ITelemetryBaseProperties,
92
+ ): void {
93
+ for (const key of Object.keys(source)) {
94
+ if (target[key] === undefined) {
95
+ target[key] = source[key];
96
+ }
97
+ }
73
98
  }
74
99
 
75
- /** Metadata to annotate an error object when annotating or normalizing it */
100
+ /**
101
+ * Metadata to annotate an error object when annotating or normalizing it
102
+ *
103
+ * @internal
104
+ */
76
105
  export interface IFluidErrorAnnotations {
77
- /** Telemetry props to log with the error */
78
- props?: ITelemetryProperties;
106
+ /**
107
+ * Telemetry props to log with the error
108
+ */
109
+ props?: ITelemetryBaseProperties;
79
110
  }
80
111
 
81
- /** For backwards compatibility with pre-errorInstanceId valid errors */
112
+ /**
113
+ * For backwards compatibility with pre-errorInstanceId valid errors
114
+ */
82
115
  function patchLegacyError(
83
- legacyError: Omit<IFluidErrorBase, "errorInstanceId">,
116
+ legacyError: Omit<IFluidErrorBase, "errorInstanceId">,
84
117
  ): asserts legacyError is IFluidErrorBase {
85
- const patchMe: { -readonly [P in "errorInstanceId"]?: IFluidErrorBase[P] } = legacyError as any;
86
- if (patchMe.errorInstanceId === undefined) {
87
- patchMe.errorInstanceId = uuid();
88
- }
118
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-explicit-any
119
+ const patchMe: { -readonly [P in "errorInstanceId"]?: IFluidErrorBase[P] } = legacyError as any;
120
+ if (patchMe.errorInstanceId === undefined) {
121
+ patchMe.errorInstanceId = uuid();
122
+ }
89
123
  }
90
124
 
91
125
  /**
@@ -93,50 +127,65 @@ function patchLegacyError(
93
127
  * @returns A valid Fluid Error with any provided annotations applied
94
128
  * @param error - The error to normalize
95
129
  * @param annotations - Annotations to apply to the normalized error
130
+ *
131
+ * @internal
96
132
  */
97
133
  export function normalizeError(
98
- error: unknown,
99
- annotations: IFluidErrorAnnotations = {},
134
+ error: unknown,
135
+ annotations: IFluidErrorAnnotations = {},
100
136
  ): IFluidErrorBase {
101
- // Back-compat, while IFluidErrorBase is rolled out
102
- if (isValidLegacyError(error)) {
103
- patchLegacyError(error);
104
- }
105
-
106
- if (isFluidError(error)) {
107
- // We can simply add the telemetry props to the error and return it
108
- error.addTelemetryProperties(annotations.props ?? {});
109
- return error;
110
- }
111
-
112
- // We have to construct a new Fluid Error, copying safe properties over
113
- const { message, stack } = extractLogSafeErrorProperties(error, false /* sanitizeStack */);
114
- const fluidError: IFluidErrorBase = new NormalizedExternalError({
115
- message,
116
- stack,
117
- });
118
-
119
- // We need to preserve these properties which are used in a non-typesafe way throughout driver code (see #8743)
120
- // Anywhere they are set should be on a valid Fluid Error that would have been returned above,
121
- // but we can't prove it with the types, so adding this defensive measure.
122
- if (typeof error === "object" && error !== null) {
123
- const { canRetry, retryAfterSeconds } = error as any;
124
- Object.assign(normalizeError, { canRetry, retryAfterSeconds });
125
- }
126
-
127
- if (typeof (error) !== "object") {
128
- // This is only interesting for non-objects
129
- fluidError.addTelemetryProperties({ typeofError: typeof (error) });
130
- }
131
-
132
- const originalErrorTelemetryProps = isILoggingError(error) ? error.getTelemetryProperties() : {};
133
- fluidError.addTelemetryProperties({
134
- ...originalErrorTelemetryProps,
135
- ...annotations.props,
136
- untrustedOrigin: 1, // This will let us filter to errors not originated by our own code
137
- });
138
-
139
- return fluidError;
137
+ // Back-compat, while IFluidErrorBase is rolled out
138
+ if (isValidLegacyError(error)) {
139
+ patchLegacyError(error);
140
+ }
141
+
142
+ if (isFluidError(error)) {
143
+ // We can simply add the telemetry props to the error and return it
144
+ error.addTelemetryProperties(annotations.props ?? {});
145
+ return error;
146
+ }
147
+
148
+ // We have to construct a new Fluid Error, copying safe properties over
149
+ const { message, stack } = extractLogSafeErrorProperties(error, false /* sanitizeStack */);
150
+ const fluidError: IFluidErrorBase = new NormalizedLoggingError({
151
+ message,
152
+ stack,
153
+ });
154
+
155
+ // We need to preserve these properties which are used in a non-typesafe way throughout driver code (see #8743)
156
+ // Anywhere they are set should be on a valid Fluid Error that would have been returned above,
157
+ // but we can't prove it with the types, so adding this defensive measure.
158
+ if (typeof error === "object" && error !== null) {
159
+ const maybeHasRetry: Partial<Record<"canRetry" | "retryAfterSeconds", unknown>> = error;
160
+ let retryProps: Partial<Record<"canRetry" | "retryAfterSeconds", unknown>> | undefined;
161
+ if ("canRetry" in error) {
162
+ retryProps ??= {};
163
+ retryProps.canRetry = maybeHasRetry.canRetry;
164
+ }
165
+ if ("retryAfterSeconds" in error) {
166
+ retryProps ??= {};
167
+ retryProps.retryAfterSeconds = maybeHasRetry.retryAfterSeconds;
168
+ }
169
+ if (retryProps !== undefined) {
170
+ Object.assign(fluidError, retryProps);
171
+ }
172
+ }
173
+
174
+ if (typeof error !== "object") {
175
+ // This is only interesting for non-objects
176
+ fluidError.addTelemetryProperties({ typeofError: typeof error });
177
+ }
178
+
179
+ const errorTelemetryProps = LoggingError.typeCheck(error)
180
+ ? error.getTelemetryProperties()
181
+ : { untrustedOrigin: 1 }; // This will let us filter errors that did not originate from our own codebase
182
+
183
+ fluidError.addTelemetryProperties({
184
+ ...errorTelemetryProps,
185
+ ...annotations.props,
186
+ });
187
+
188
+ return fluidError;
140
189
  }
141
190
 
142
191
  let stackPopulatedOnCreation: boolean | undefined;
@@ -149,27 +198,35 @@ let stackPopulatedOnCreation: boolean | undefined;
149
198
  * For such cases it's better to not read stack property right away, but rather delay it until / if it's needed
150
199
  * Some browsers will populate stack right away, others require throwing Error, so we do auto-detection on the fly.
151
200
  * @returns Error object that has stack populated.
201
+ *
202
+ * @internal
152
203
  */
153
- export function generateErrorWithStack(): Error {
154
- const err = new Error("<<generated stack>>");
155
-
156
- if (stackPopulatedOnCreation === undefined) {
157
- stackPopulatedOnCreation = (err.stack !== undefined);
158
- }
159
-
160
- if (stackPopulatedOnCreation) {
161
- return err;
162
- }
163
-
164
- try {
165
- throw err;
166
- } catch (e) {
167
- return e as Error;
168
- }
204
+ export function generateErrorWithStack(): Error {
205
+ const err = new Error("<<generated stack>>");
206
+
207
+ if (stackPopulatedOnCreation === undefined) {
208
+ stackPopulatedOnCreation = err.stack !== undefined;
209
+ }
210
+
211
+ if (stackPopulatedOnCreation) {
212
+ return err;
213
+ }
214
+
215
+ try {
216
+ throw err;
217
+ } catch (error) {
218
+ return error as Error;
219
+ }
169
220
  }
170
221
 
222
+ /**
223
+ * Generate a stack at this callsite as if an error were thrown from here.
224
+ * @returns the callstack (does not throw)
225
+ *
226
+ * @internal
227
+ */
171
228
  export function generateStack(): string | undefined {
172
- return generateErrorWithStack().stack;
229
+ return generateErrorWithStack().stack;
173
230
  }
174
231
 
175
232
  /**
@@ -178,122 +235,178 @@ export function generateStack(): string | undefined {
178
235
  * @param innerError - An error from untrusted/unknown origins
179
236
  * @param newErrorFn - callback that will create a new error given the original error's message
180
237
  * @returns A new error object "wrapping" the given error
238
+ *
239
+ * @internal
181
240
  */
182
241
  export function wrapError<T extends LoggingError>(
183
- innerError: unknown,
184
- newErrorFn: (message: string) => T,
242
+ innerError: unknown,
243
+ newErrorFn: (message: string) => T,
185
244
  ): T {
186
- const {
187
- message,
188
- stack,
189
- } = extractLogSafeErrorProperties(innerError, false /* sanitizeStack */);
245
+ const { message, stack } = extractLogSafeErrorProperties(innerError, false /* sanitizeStack */);
190
246
 
191
- const newError = newErrorFn(message);
247
+ const newError = newErrorFn(message);
192
248
 
193
- if (stack !== undefined) {
194
- overwriteStack(newError, stack);
195
- }
249
+ if (stack !== undefined) {
250
+ overwriteStack(newError, stack);
251
+ }
196
252
 
197
- // Mark external errors with untrustedOrigin flag
198
- if (isExternalError(innerError)) {
199
- newError.addTelemetryProperties({ untrustedOrigin: 1 });
200
- }
253
+ // Mark external errors with untrustedOrigin flag
254
+ if (isExternalError(innerError)) {
255
+ newError.addTelemetryProperties({ untrustedOrigin: 1 });
256
+ }
201
257
 
202
- // Reuse errorInstanceId
203
- if (hasErrorInstanceId(innerError)) {
204
- newError.overwriteErrorInstanceId(innerError.errorInstanceId);
258
+ // Reuse errorInstanceId
259
+ if (hasErrorInstanceId(innerError)) {
260
+ newError.overwriteErrorInstanceId(innerError.errorInstanceId);
205
261
 
206
- // For "back-compat" in the logs
207
- newError.addTelemetryProperties({ innerErrorInstanceId: innerError.errorInstanceId });
208
- }
262
+ // For "back-compat" in the logs
263
+ newError.addTelemetryProperties({ innerErrorInstanceId: innerError.errorInstanceId });
264
+ }
209
265
 
210
- // Lastly, copy over all other telemetry properties. Note these will not overwrite existing properties
211
- // This will include the untrustedOrigin property if the inner error itself was created from an external error
212
- if (isILoggingError(innerError)) {
213
- newError.addTelemetryProperties(innerError.getTelemetryProperties());
214
- }
266
+ // Lastly, copy over all other telemetry properties. Note these will not overwrite existing properties
267
+ // This will include the untrustedOrigin property if the inner error itself was created from an external error
268
+ if (isILoggingError(innerError)) {
269
+ newError.addTelemetryProperties(innerError.getTelemetryProperties());
270
+ }
215
271
 
216
- return newError;
272
+ return newError;
217
273
  }
218
274
 
219
- /** The same as wrapError, but also logs the innerError, including the wrapping error's instance id */
275
+ /**
276
+ * The same as wrapError, but also logs the innerError, including the wrapping error's instance ID.
277
+ *
278
+ * @typeParam T - The kind of wrapper error to create.
279
+ *
280
+ * @internal
281
+ */
220
282
  export function wrapErrorAndLog<T extends LoggingError>(
221
- innerError: unknown,
222
- newErrorFn: (message: string) => T,
223
- logger: ITelemetryLogger,
224
- ) {
225
- const newError = wrapError(innerError, newErrorFn);
283
+ innerError: unknown,
284
+ newErrorFn: (message: string) => T,
285
+ logger: ITelemetryLoggerExt,
286
+ ): T {
287
+ const newError = wrapError(innerError, newErrorFn);
226
288
 
227
- // This will match innerError.errorInstanceId if present (see wrapError)
228
- const errorInstanceId = newError.errorInstanceId;
289
+ // This will match innerError.errorInstanceId if present (see wrapError)
290
+ const errorInstanceId = newError.errorInstanceId;
229
291
 
230
- // For "back-compat" in the logs
231
- const wrappedByErrorInstanceId = errorInstanceId;
292
+ // For "back-compat" in the logs
293
+ const wrappedByErrorInstanceId = errorInstanceId;
232
294
 
233
- logger.sendTelemetryEvent({
234
- eventName: "WrapError",
235
- errorInstanceId,
236
- wrappedByErrorInstanceId,
237
- }, innerError);
295
+ logger.sendTelemetryEvent(
296
+ {
297
+ eventName: "WrapError",
298
+ errorInstanceId,
299
+ wrappedByErrorInstanceId,
300
+ },
301
+ innerError,
302
+ );
238
303
 
239
- return newError;
304
+ return newError;
240
305
  }
241
306
 
242
- function overwriteStack(error: IFluidErrorBase | LoggingError, stack: string) {
243
- // supposedly setting stack on an Error can throw.
244
- try {
245
- Object.assign(error, { stack });
246
- } catch (errorSettingStack) {
247
- error.addTelemetryProperties({ stack2: stack });
248
- }
307
+ /**
308
+ * Attempts to overwrite the error's stack
309
+ *
310
+ * There have been reports of certain JS environments where overwriting stack will throw.
311
+ * If that happens, this adds the given stack as the telemetry property "stack2"
312
+ *
313
+ * @internal
314
+ */
315
+ export function overwriteStack(error: IFluidErrorBase | LoggingError, stack: string): void {
316
+ try {
317
+ Object.assign(error, { stack });
318
+ } catch {
319
+ error.addTelemetryProperties({ stack2: stack });
320
+ }
249
321
  }
250
322
 
251
323
  /**
252
324
  * True for any error object that is an (optionally normalized) external error
253
- * False for any error we created and raised within the FF codebase, or wrapped in a well-known error type
325
+ * False for any error we created and raised within the FF codebase via LoggingError base class,
326
+ * or wrapped in a well-known error type
327
+ *
328
+ * @internal
329
+ */
330
+ export function isExternalError(error: unknown): boolean {
331
+ // LoggingErrors are an internal FF error type. However, an external error can be converted
332
+ // into a LoggingError if it is normalized. In this case we must use the untrustedOrigin flag to
333
+ // determine whether the original error was infact external.
334
+ if (LoggingError.typeCheck(error)) {
335
+ if ((error as NormalizedLoggingError).errorType === NORMALIZED_ERROR_TYPE) {
336
+ return error.getTelemetryProperties().untrustedOrigin === 1;
337
+ }
338
+ return false;
339
+ }
340
+ return !isValidLegacyError(error);
341
+ }
342
+
343
+ /**
344
+ * Type guard to identify if a particular telemetry property appears to be a
345
+ * {@link @fluidframework/core-interfaces#Tagged} telemetry property.
346
+ *
347
+ * @internal
254
348
  */
255
- export function isExternalError(e: any): boolean {
256
- return !isValidLegacyError(e) ||
257
- (e.getTelemetryProperties().untrustedOrigin === 1 &&
258
- e.errorType === NormalizedExternalError.normalizedErrorType);
349
+ export function isTaggedTelemetryPropertyValue(
350
+ x: Tagged<TelemetryEventPropertyTypeExt> | TelemetryEventPropertyTypeExt,
351
+ ): x is Tagged<TelemetryEventPropertyTypeExt> {
352
+ return typeof (x as Partial<Tagged<unknown>>)?.tag === "string";
259
353
  }
260
354
 
261
355
  /**
262
- * Type guard to identify if a particular value (loosely) appears to be a tagged telemetry property
356
+ * Filter serializable telemetry properties
357
+ * @param x - Any telemetry prop
358
+ * @returns As-is if x is primitive. returns stringified if x is an array of primitive.
359
+ * otherwise returns null since this is what we support at the moment.
263
360
  */
264
- export function isTaggedTelemetryPropertyValue(x: any): x is ITaggedTelemetryPropertyType {
265
- return (typeof (x?.value) !== "object" && typeof (x?.tag) === "string");
361
+ function filterValidTelemetryProps(x: unknown, key: string): TelemetryBaseEventPropertyType {
362
+ if (Array.isArray(x) && x.every((val) => isTelemetryEventPropertyValue(val))) {
363
+ return JSON.stringify(x);
364
+ }
365
+ if (isTelemetryEventPropertyValue(x)) {
366
+ return x;
367
+ }
368
+ // We don't support logging arbitrary objects
369
+ console.error(`UnSupported Format of Logging Error Property for key ${key}:`, x);
370
+ return "REDACTED (arbitrary object)";
371
+ }
372
+
373
+ // checking type of x, returns false if x is null
374
+ function isTelemetryEventPropertyValue(x: unknown): x is TelemetryBaseEventPropertyType {
375
+ switch (typeof x) {
376
+ case "string":
377
+ case "number":
378
+ case "boolean":
379
+ case "undefined": {
380
+ return true;
381
+ }
382
+ default: {
383
+ return false;
384
+ }
385
+ }
266
386
  }
267
387
 
268
388
  /**
269
389
  * Walk an object's enumerable properties to find those fit for telemetry.
270
390
  */
271
- function getValidTelemetryProps(obj: any, keysToOmit: Set<string>): ITelemetryProperties {
272
- const props: ITelemetryProperties = {};
273
- for (const key of Object.keys(obj)) {
274
- if (keysToOmit.has(key)) {
275
- continue;
276
- }
277
- const val = obj[key];
278
- switch (typeof val) {
279
- case "string":
280
- case "number":
281
- case "boolean":
282
- case "undefined":
283
- props[key] = val;
284
- break;
285
- default: {
286
- if (isTaggedTelemetryPropertyValue(val)) {
287
- props[key] = val;
288
- } else {
289
- // We don't support logging arbitrary objects
290
- props[key] = "REDACTED (arbitrary object)";
291
- }
292
- break;
293
- }
294
- }
295
- }
296
- return props;
391
+ function getValidTelemetryProps(obj: object, keysToOmit: Set<string>): ITelemetryBaseProperties {
392
+ const props: ITelemetryBaseProperties = {};
393
+ for (const key of Object.keys(obj)) {
394
+ if (keysToOmit.has(key)) {
395
+ continue;
396
+ }
397
+ const val = obj[key] as
398
+ | TelemetryEventPropertyTypeExt
399
+ | Tagged<TelemetryEventPropertyTypeExt>;
400
+
401
+ // ensure only valid props get logged, since props of logging error could be in any shape
402
+ props[key] = isTaggedTelemetryPropertyValue(val)
403
+ ? {
404
+ value: filterValidTelemetryProps(val.value, key),
405
+ tag: val.tag,
406
+ }
407
+ : filterValidTelemetryProps(val, key);
408
+ }
409
+ return props;
297
410
  }
298
411
 
299
412
  /**
@@ -302,19 +415,24 @@ function getValidTelemetryProps(obj: any, keysToOmit: Set<string>): ITelemetryPr
302
415
  * Avoids runtime errors with circular references.
303
416
  * Not ideal, as will cut values that are not necessarily circular references.
304
417
  * Could be improved by implementing Node's util.inspect() for browser (minus all the coloring code)
305
- */
306
- export const getCircularReplacer = () => {
307
- const seen = new WeakSet();
308
- return (key: string, value: any): any => {
309
- if (typeof value === "object" && value !== null) {
310
- if (seen.has(value)) {
311
- return "<removed/circular>";
312
- }
313
- seen.add(value);
314
- }
315
- return value;
316
- };
418
+ *
419
+ * @internal
420
+ */
421
+ // TODO: Use `unknown` instead (API breaking change)
422
+ /* eslint-disable @typescript-eslint/no-explicit-any */
423
+ export const getCircularReplacer = (): ((key: string, value: unknown) => any) => {
424
+ const seen = new WeakSet();
425
+ return (key: string, value: unknown): any => {
426
+ if (typeof value === "object" && value !== null) {
427
+ if (seen.has(value)) {
428
+ return "<removed/circular>";
429
+ }
430
+ seen.add(value);
431
+ }
432
+ return value;
433
+ };
317
434
  };
435
+ /* eslint-enable @typescript-eslint/no-explicit-any */
318
436
 
319
437
  /**
320
438
  * Base class for "trusted" errors we create, whose properties can generally be logged to telemetry safely.
@@ -322,78 +440,110 @@ export const getCircularReplacer = () => {
322
440
  * will be logged in accordance with their tag, if present.
323
441
  *
324
442
  * PLEASE take care to avoid setting sensitive data on this object without proper tagging!
443
+ *
444
+ * @internal
325
445
  */
326
- export class LoggingError extends Error implements ILoggingError, Omit<IFluidErrorBase, "errorType"> {
327
- private _errorInstanceId = uuid();
328
- get errorInstanceId() { return this._errorInstanceId; }
329
- overwriteErrorInstanceId(id: string) { this._errorInstanceId = id; }
330
-
331
- /** Back-compat to appease isFluidError typeguard in old code that may handle this error */
332
- // @ts-expect-error - This field shouldn't be referenced in the current version, but needs to exist at runtime.
333
- private readonly fluidErrorCode: "-" = "-";
334
-
335
- /**
336
- * Create a new LoggingError
337
- * @param message - Error message to use for Error base class
338
- * @param props - telemetry props to include on the error for when it's logged
339
- * @param omitPropsFromLogging - properties by name to omit from telemetry props
340
- */
341
- constructor(
342
- message: string,
343
- props?: ITelemetryProperties,
344
- private readonly omitPropsFromLogging: Set<string> = new Set(),
345
- ) {
346
- super(message);
347
-
348
- // Don't log this list itself, or the private _errorInstanceId
349
- omitPropsFromLogging.add("omitPropsFromLogging");
350
- omitPropsFromLogging.add("_errorInstanceId");
351
-
352
- if (props) {
353
- this.addTelemetryProperties(props);
354
- }
355
- }
356
-
357
- /**
358
- * Add additional properties to be logged
359
- */
360
- public addTelemetryProperties(props: ITelemetryProperties) {
361
- copyProps(this, props);
362
- }
363
-
364
- /**
365
- * Get all properties fit to be logged to telemetry for this error
366
- */
367
- public getTelemetryProperties(): ITelemetryProperties {
368
- const taggableProps = getValidTelemetryProps(this, this.omitPropsFromLogging);
369
- // Include non-enumerable props that are not returned by getValidTelemetryProps
370
- return {
371
- ...taggableProps,
372
- stack: this.stack,
373
- message: this.message,
374
- errorInstanceId: this._errorInstanceId,
375
- };
376
- }
446
+ export class LoggingError
447
+ extends Error
448
+ implements ILoggingError, Omit<IFluidErrorBase, "errorType">
449
+ {
450
+ private _errorInstanceId = uuid();
451
+ get errorInstanceId(): string {
452
+ return this._errorInstanceId;
453
+ }
454
+ overwriteErrorInstanceId(id: string): void {
455
+ this._errorInstanceId = id;
456
+ }
457
+
458
+ /**
459
+ * Backwards compatibility to appease {@link isFluidError} in old code that may handle this error.
460
+ */
461
+ // @ts-expect-error - This field shouldn't be referenced in the current version, but needs to exist at runtime.
462
+ // eslint-disable-next-line @typescript-eslint/prefer-as-const
463
+ private readonly fluidErrorCode: "-" = "-";
464
+
465
+ /**
466
+ * Create a new LoggingError
467
+ * @param message - Error message to use for Error base class
468
+ * @param props - telemetry props to include on the error for when it's logged
469
+ * @param omitPropsFromLogging - properties by name to omit from telemetry props
470
+ */
471
+ constructor(
472
+ message: string,
473
+ props?: ITelemetryBaseProperties,
474
+ private readonly omitPropsFromLogging: Set<string> = new Set(),
475
+ ) {
476
+ super(message);
477
+
478
+ // Don't log this list itself, or the private _errorInstanceId
479
+ omitPropsFromLogging.add("omitPropsFromLogging");
480
+ omitPropsFromLogging.add("_errorInstanceId");
481
+
482
+ if (props) {
483
+ this.addTelemetryProperties(props);
484
+ }
485
+ }
486
+
487
+ /**
488
+ * Determines if a given object is an instance of a LoggingError
489
+ * @param object - any object
490
+ * @returns true if the object is an instance of a LoggingError, false if not.
491
+ */
492
+ public static typeCheck(object: unknown): object is LoggingError {
493
+ if (typeof object === "object" && object !== null) {
494
+ return (
495
+ typeof (object as LoggingError).addTelemetryProperties === "function" &&
496
+ typeof (object as LoggingError).getTelemetryProperties === "function" &&
497
+ typeof (object as LoggingError).errorInstanceId === "string"
498
+ );
499
+ }
500
+ return false;
501
+ }
502
+
503
+ /**
504
+ * Add additional properties to be logged
505
+ */
506
+ public addTelemetryProperties(props: ITelemetryBaseProperties): void {
507
+ copyProps(this, props);
508
+ }
509
+
510
+ /**
511
+ * Get all properties fit to be logged to telemetry for this error
512
+ */
513
+ public getTelemetryProperties(): ITelemetryBaseProperties {
514
+ const taggableProps = getValidTelemetryProps(this, this.omitPropsFromLogging);
515
+ // Include non-enumerable props that are not returned by getValidTelemetryProps
516
+ return {
517
+ ...taggableProps,
518
+ stack: this.stack,
519
+ message: this.message,
520
+ errorInstanceId: this._errorInstanceId,
521
+ };
522
+ }
377
523
  }
378
524
 
379
- /** The Error class used when normalizing an external error */
380
- class NormalizedExternalError extends LoggingError {
381
- // errorType "genericError" is used as a default value throughout the code.
382
- // Note that this matches ContainerErrorType/DriverErrorType's genericError
383
- static readonly normalizedErrorType = "genericError";
384
-
385
- errorType = NormalizedExternalError.normalizedErrorType;
386
-
387
- constructor(
388
- errorProps: Pick<IFluidErrorBase,
389
- | "message"
390
- | "stack"
391
- >,
392
- ) {
393
- super(errorProps.message);
394
-
395
- if (errorProps.stack !== undefined) {
396
- overwriteStack(this, errorProps.stack);
397
- }
398
- }
525
+ /**
526
+ * The Error class used when normalizing an external error
527
+ *
528
+ * @internal
529
+ */
530
+ export const NORMALIZED_ERROR_TYPE = "genericError";
531
+
532
+ /**
533
+ * Subclass of LoggingError returned by normalizeError
534
+ *
535
+ * @internal
536
+ */
537
+ class NormalizedLoggingError extends LoggingError {
538
+ // errorType "genericError" is used as a default value throughout the code.
539
+ // Note that this matches ContainerErrorType/DriverErrorType's genericError
540
+ errorType = NORMALIZED_ERROR_TYPE;
541
+
542
+ constructor(errorProps: Pick<IFluidErrorBase, "message" | "stack">) {
543
+ super(errorProps.message);
544
+
545
+ if (errorProps.stack !== undefined) {
546
+ overwriteStack(this, errorProps.stack);
547
+ }
548
+ }
399
549
  }