@fluidframework/telemetry-utils 0.57.1 → 0.58.0-55561

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.
@@ -78,16 +78,20 @@ export interface IFluidErrorAnnotations {
78
78
  props?: ITelemetryProperties;
79
79
  }
80
80
 
81
- /** For backwards compatibility with pre-fluidErrorCode valid errors */
82
- function patchWithErrorCode(
83
- legacyError: Omit<IFluidErrorBase, "fluidErrorCode">,
81
+ /** For backwards compatibility with pre-errorInstanceId valid errors */
82
+ function patchLegacyError(
83
+ legacyError: Omit<IFluidErrorBase, "errorInstanceId">,
84
84
  ): asserts legacyError is IFluidErrorBase {
85
- const patchMe: { -readonly [P in "fluidErrorCode"]?: IFluidErrorBase[P] } = legacyError as any;
86
- if (patchMe.fluidErrorCode === undefined) {
87
- patchMe.fluidErrorCode = "<error predates fluidErrorCode>";
85
+ const patchMe: { -readonly [P in "errorInstanceId"]?: IFluidErrorBase[P] } = legacyError as any;
86
+ if (patchMe.errorInstanceId === undefined) {
87
+ patchMe.errorInstanceId = uuid();
88
88
  }
89
89
  }
90
90
 
91
+ // errorType "genericError" is used as a default value throughout the code.
92
+ // Note that this matches ContainerErrorType/DriverErrorType's genericError
93
+ const defaultErrorTypeForNormalize = "genericError";
94
+
91
95
  /**
92
96
  * Normalize the given error yielding a valid Fluid Error
93
97
  * @returns A valid Fluid Error with any provided annotations applied
@@ -100,7 +104,7 @@ export function normalizeError(
100
104
  ): IFluidErrorBase {
101
105
  // Back-compat, while IFluidErrorBase is rolled out
102
106
  if (isValidLegacyError(error)) {
103
- patchWithErrorCode(error);
107
+ patchLegacyError(error);
104
108
  }
105
109
 
106
110
  if (isFluidError(error)) {
@@ -112,8 +116,7 @@ export function normalizeError(
112
116
  // We have to construct a new Fluid Error, copying safe properties over
113
117
  const { message, stack } = extractLogSafeErrorProperties(error, false /* sanitizeStack */);
114
118
  const fluidError: IFluidErrorBase = new SimpleFluidError({
115
- errorType: "genericError", // Match Container/Driver generic error type
116
- fluidErrorCode: "",
119
+ errorType: defaultErrorTypeForNormalize,
117
120
  message,
118
121
  stack,
119
122
  });
@@ -171,7 +174,7 @@ export function generateStack(): string | undefined {
171
174
  * @param newErrorFn - callback that will create a new error given the original error's message
172
175
  * @returns A new error object "wrapping" the given error
173
176
  */
174
- export function wrapError<T extends IFluidErrorBase>(
177
+ export function wrapError<T extends LoggingError>(
175
178
  innerError: unknown,
176
179
  newErrorFn: (message: string) => T,
177
180
  ): T {
@@ -186,7 +189,16 @@ export function generateStack(): string | undefined {
186
189
  overwriteStack(newError, stack);
187
190
  }
188
191
 
192
+ // Mark external errors with untrustedOrigin flag
193
+ if (originatedAsExternalError(innerError)) {
194
+ newError.addTelemetryProperties({ untrustedOrigin: 1 });
195
+ }
196
+
197
+ // Reuse errorInstanceId
189
198
  if (hasErrorInstanceId(innerError)) {
199
+ newError.overwriteErrorInstanceId(innerError.errorInstanceId);
200
+
201
+ // For "back-compat" in the logs
190
202
  newError.addTelemetryProperties({ innerErrorInstanceId: innerError.errorInstanceId });
191
203
  }
192
204
 
@@ -194,25 +206,29 @@ export function generateStack(): string | undefined {
194
206
  }
195
207
 
196
208
  /** The same as wrapError, but also logs the innerError, including the wrapping error's instance id */
197
- export function wrapErrorAndLog<T extends IFluidErrorBase>(
209
+ export function wrapErrorAndLog<T extends LoggingError>(
198
210
  innerError: unknown,
199
211
  newErrorFn: (message: string) => T,
200
212
  logger: ITelemetryLogger,
201
213
  ) {
202
214
  const newError = wrapError(innerError, newErrorFn);
203
- const wrappedByErrorInstanceId = hasErrorInstanceId(newError)
204
- ? newError.errorInstanceId
205
- : undefined;
215
+
216
+ // This will match innerError.errorInstanceId if present (see wrapError)
217
+ const errorInstanceId = newError.errorInstanceId;
218
+
219
+ // For "back-compat" in the logs
220
+ const wrappedByErrorInstanceId = errorInstanceId;
206
221
 
207
222
  logger.sendTelemetryEvent({
208
223
  eventName: "WrapError",
224
+ errorInstanceId,
209
225
  wrappedByErrorInstanceId,
210
226
  }, innerError);
211
227
 
212
228
  return newError;
213
229
  }
214
230
 
215
- function overwriteStack(error: IFluidErrorBase, stack: string) {
231
+ function overwriteStack(error: IFluidErrorBase | LoggingError, stack: string) {
216
232
  // supposedly setting stack on an Error can throw.
217
233
  try {
218
234
  Object.assign(error, { stack });
@@ -221,6 +237,24 @@ function overwriteStack(error: IFluidErrorBase, stack: string) {
221
237
  }
222
238
  }
223
239
 
240
+ /**
241
+ * True for any error object that is either external itself or is a wrapped/normalized external error
242
+ * False for any error we created and raised within the FF codebase.
243
+ */
244
+ export function originatedAsExternalError(e: any): boolean {
245
+ return !isValidLegacyError(e) || (e.getTelemetryProperties().untrustedOrigin === 1);
246
+ }
247
+
248
+ /**
249
+ * True for any error object that is an (optionally normalized) external error
250
+ * False for any error we created and raised within the FF codebase, or wrapped in a well-known error type
251
+ */
252
+ export function isExternalError(e: any): boolean {
253
+ return !isValidLegacyError(e) ||
254
+ (e.getTelemetryProperties().untrustedOrigin === 1 &&
255
+ e.errorType === defaultErrorTypeForNormalize);
256
+ }
257
+
224
258
  /**
225
259
  * Type guard to identify if a particular value (loosely) appears to be a tagged telemetry property
226
260
  */
@@ -287,7 +321,13 @@ export const getCircularReplacer = () => {
287
321
  * PLEASE take care to avoid setting sensitive data on this object without proper tagging!
288
322
  */
289
323
  export class LoggingError extends Error implements ILoggingError, Pick<IFluidErrorBase, "errorInstanceId"> {
290
- readonly errorInstanceId = uuid();
324
+ private _errorInstanceId = uuid();
325
+ get errorInstanceId() { return this._errorInstanceId; }
326
+ overwriteErrorInstanceId(id: string) { this._errorInstanceId = id; }
327
+
328
+ /** Back-compat to appease isFluidError typeguard in old code that may handle this error */
329
+ // @ts-expect-error - This field shouldn't be referenced in the current version, but needs to exist at runtime.
330
+ private fluidErrorCode: "-" = "-";
291
331
 
292
332
  /**
293
333
  * Create a new LoggingError
@@ -302,8 +342,9 @@ export class LoggingError extends Error implements ILoggingError, Pick<IFluidErr
302
342
  ) {
303
343
  super(message);
304
344
 
305
- // Don't log this list itself either
345
+ // Don't log this list itself, or the private _errorInstanceId
306
346
  omitPropsFromLogging.add("omitPropsFromLogging");
347
+ omitPropsFromLogging.add("_errorInstanceId");
307
348
 
308
349
  if (props) {
309
350
  this.addTelemetryProperties(props);
@@ -322,11 +363,12 @@ export class LoggingError extends Error implements ILoggingError, Pick<IFluidErr
322
363
  */
323
364
  public getTelemetryProperties(): ITelemetryProperties {
324
365
  const taggableProps = getValidTelemetryProps(this, this.omitPropsFromLogging);
325
- // Include non-enumerable props inherited from Error that are not returned by getValidTelemetryProps
366
+ // Include non-enumerable props that are not returned by getValidTelemetryProps
326
367
  return {
327
368
  ...taggableProps,
328
369
  stack: this.stack,
329
370
  message: this.message,
371
+ errorInstanceId: this._errorInstanceId,
330
372
  };
331
373
  }
332
374
  }
@@ -334,18 +376,16 @@ export class LoggingError extends Error implements ILoggingError, Pick<IFluidErr
334
376
  /** Simple implementation of IFluidErrorBase, extending LoggingError */
335
377
  class SimpleFluidError extends LoggingError implements IFluidErrorBase {
336
378
  readonly errorType: string;
337
- readonly fluidErrorCode: string;
338
379
 
339
380
  constructor(
340
- errorProps: Omit<IFluidErrorBase,
341
- | "getTelemetryProperties"
342
- | "addTelemetryProperties"
343
- | "errorInstanceId"
344
- | "name">,
381
+ errorProps: Pick<IFluidErrorBase,
382
+ | "message"
383
+ | "stack"
384
+ | "errorType"
385
+ >,
345
386
  ) {
346
387
  super(errorProps.message);
347
388
  this.errorType = errorProps.errorType;
348
- this.fluidErrorCode = errorProps.fluidErrorCode;
349
389
  if (errorProps.stack !== undefined) {
350
390
  overwriteStack(this, errorProps.stack);
351
391
  }
@@ -7,7 +7,7 @@ import { ITelemetryProperties } from "@fluidframework/common-definitions";
7
7
 
8
8
  /**
9
9
  * All normalized errors flowing through the Fluid Framework adhere to this readonly interface.
10
- * It features errorType, fluidErrorCode, and message strings, plus Error's members as optional
10
+ * It features errorType and errorInstanceId on top of Error's members as readonly,
11
11
  * and a getter/setter for telemetry props to be included when the error is logged.
12
12
  */
13
13
  export interface IFluidErrorBase extends Error {
@@ -15,12 +15,10 @@ export interface IFluidErrorBase extends Error {
15
15
  readonly errorType: string;
16
16
 
17
17
  /**
18
- * Indicates a point in code where this error originated.
19
- * Avoid crafting these via string format or otherwise including variable data, so they're easy to find the code.
18
+ * Error's message property, made readonly.
19
+ * Be specific, but also take care when including variable data to consider suitability for aggregation in telemetry
20
+ * Also avoid including any data that jeopardizes the user's privacy. Add a tagged telemetry property instead.
20
21
  */
21
- readonly fluidErrorCode: string;
22
-
23
- /** The free-form error message */
24
22
  readonly message: string;
25
23
 
26
24
  /** Error's stack property, made readonly */
@@ -52,14 +50,13 @@ export const hasErrorInstanceId = (x: any): x is { errorInstanceId: string } =>
52
50
  /** type guard for IFluidErrorBase interface */
53
51
  export function isFluidError(e: any): e is IFluidErrorBase {
54
52
  return typeof e?.errorType === "string" &&
55
- typeof e?.fluidErrorCode === "string" &&
56
53
  typeof e?.message === "string" &&
57
- typeof e?.errorInstanceId === "string" &&
54
+ hasErrorInstanceId(e) &&
58
55
  hasTelemetryPropFunctions(e);
59
56
  }
60
57
 
61
58
  /** type guard for old standard of valid/known errors */
62
- export function isValidLegacyError(e: any): e is Omit<IFluidErrorBase, "fluidErrorCode"> {
59
+ export function isValidLegacyError(e: any): e is Omit<IFluidErrorBase, "errorInstanceId"> {
63
60
  return typeof e?.errorType === "string" &&
64
61
  typeof e?.message === "string" &&
65
62
  hasTelemetryPropFunctions(e);
package/src/mockLogger.ts CHANGED
@@ -4,7 +4,6 @@
4
4
  */
5
5
 
6
6
  import { ITelemetryLogger, ITelemetryBaseEvent } from "@fluidframework/common-definitions";
7
- import { assert } from "@fluidframework/common-utils";
8
7
  import { TelemetryLogger } from "./logger";
9
8
 
10
9
  /**
@@ -35,14 +34,16 @@ export class MockLogger extends TelemetryLogger implements ITelemetryLogger {
35
34
  }
36
35
 
37
36
  /** Asserts that matchEvents is true, and prints the actual/expected output if not */
38
- assertMatch(expectedEvents: Omit<ITelemetryBaseEvent, "category">[]) {
39
- // const actualEvents = this.events;
40
- assert(this.matchEvents(expectedEvents), 0x2ba /* `
41
- expected:
42
- ${JSON.stringify(expectedEvents)}
37
+ assertMatch(expectedEvents: Omit<ITelemetryBaseEvent, "category">[], message?: string) {
38
+ const actualEvents = this.events;
39
+ if (!this.matchEvents(expectedEvents)) {
40
+ throw new Error(`${message}
41
+ expected:
42
+ ${JSON.stringify(expectedEvents)}
43
43
 
44
- actual:
45
- ${JSON.stringify(actualEvents)}` */);
44
+ actual:
45
+ ${JSON.stringify(actualEvents)}`);
46
+ }
46
47
  }
47
48
 
48
49
  /**
@@ -59,15 +60,16 @@ export class MockLogger extends TelemetryLogger implements ITelemetryLogger {
59
60
  }
60
61
 
61
62
  /** Asserts that matchAnyEvent is true, and prints the actual/expected output if not */
62
- assertMatchAny(expectedEvents: Omit<ITelemetryBaseEvent, "category">[]) {
63
- // const actualEvents = this.events;
63
+ assertMatchAny(expectedEvents: Omit<ITelemetryBaseEvent, "category">[], message?: string) {
64
+ const actualEvents = this.events;
65
+ if (!this.matchAnyEvent(expectedEvents)) {
66
+ throw new Error(`${message}
67
+ expected:
68
+ ${JSON.stringify(expectedEvents)}
64
69
 
65
- assert(this.matchAnyEvent(expectedEvents), 0x2bb /* `
66
- expected:
67
- ${JSON.stringify(expectedEvents)}
68
-
69
- actual:
70
- ${JSON.stringify(actualEvents)}` */);
70
+ actual:
71
+ ${JSON.stringify(actualEvents)}`);
72
+ }
71
73
  }
72
74
 
73
75
  private getMatchedEventsCount(expectedEvents: Omit<ITelemetryBaseEvent, "category">[]): number {
@@ -6,4 +6,4 @@
6
6
  */
7
7
 
8
8
  export const pkgName = "@fluidframework/telemetry-utils";
9
- export const pkgVersion = "0.57.1";
9
+ export const pkgVersion = "0.58.0-55561";