@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.
- package/.eslintrc.js +12 -13
- package/.mocharc.js +12 -0
- package/CHANGELOG.md +249 -0
- package/README.md +68 -1
- package/api-extractor-esm.json +5 -0
- package/api-extractor-lint.json +4 -0
- package/api-extractor.json +2 -2
- package/api-report/telemetry-utils.api.md +444 -0
- package/dist/config.d.ts +47 -16
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +88 -38
- package/dist/config.js.map +1 -1
- package/dist/error.d.ts +112 -0
- package/dist/error.d.ts.map +1 -0
- package/dist/error.js +159 -0
- package/dist/error.js.map +1 -0
- package/dist/errorLogging.d.ts +86 -20
- package/dist/errorLogging.d.ts.map +1 -1
- package/dist/errorLogging.js +190 -60
- package/dist/errorLogging.js.map +1 -1
- package/dist/eventEmitterWithErrorHandling.d.ts +9 -3
- package/dist/eventEmitterWithErrorHandling.d.ts.map +1 -1
- package/dist/eventEmitterWithErrorHandling.js +16 -3
- package/dist/eventEmitterWithErrorHandling.js.map +1 -1
- package/dist/events.d.ts +27 -3
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js +26 -2
- package/dist/events.js.map +1 -1
- package/dist/fluidErrorBase.d.ts +57 -16
- package/dist/fluidErrorBase.d.ts.map +1 -1
- package/dist/fluidErrorBase.js +27 -14
- package/dist/fluidErrorBase.js.map +1 -1
- package/dist/index.d.ts +12 -11
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +55 -21
- package/dist/index.js.map +1 -1
- package/dist/logger.d.ts +269 -53
- package/dist/logger.d.ts.map +1 -1
- package/dist/logger.js +423 -132
- package/dist/logger.js.map +1 -1
- package/dist/mockLogger.d.ts +39 -12
- package/dist/mockLogger.d.ts.map +1 -1
- package/dist/mockLogger.js +105 -22
- package/dist/mockLogger.js.map +1 -1
- package/dist/sampledTelemetryHelper.d.ts +18 -12
- package/dist/sampledTelemetryHelper.d.ts.map +1 -1
- package/dist/sampledTelemetryHelper.js +28 -19
- package/dist/sampledTelemetryHelper.js.map +1 -1
- package/dist/telemetry-utils-alpha.d.ts +290 -0
- package/dist/telemetry-utils-beta.d.ts +264 -0
- package/dist/telemetry-utils-public.d.ts +264 -0
- package/dist/telemetry-utils-untrimmed.d.ts +1102 -0
- package/dist/telemetryTypes.d.ts +115 -0
- package/dist/telemetryTypes.d.ts.map +1 -0
- package/dist/telemetryTypes.js +7 -0
- package/dist/telemetryTypes.js.map +1 -0
- package/dist/thresholdCounter.d.ts +6 -5
- package/dist/thresholdCounter.d.ts.map +1 -1
- package/dist/thresholdCounter.js +4 -3
- package/dist/thresholdCounter.js.map +1 -1
- package/dist/tsdoc-metadata.json +11 -0
- package/dist/utils.d.ts +54 -3
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +58 -3
- package/dist/utils.js.map +1 -1
- package/lib/config.d.ts +47 -16
- package/lib/config.d.ts.map +1 -1
- package/lib/config.js +85 -36
- package/lib/config.js.map +1 -1
- package/lib/error.d.ts +112 -0
- package/lib/error.d.ts.map +1 -0
- package/lib/error.js +150 -0
- package/lib/error.js.map +1 -0
- package/lib/errorLogging.d.ts +86 -20
- package/lib/errorLogging.d.ts.map +1 -1
- package/lib/errorLogging.js +189 -60
- package/lib/errorLogging.js.map +1 -1
- package/lib/eventEmitterWithErrorHandling.d.ts +9 -3
- package/lib/eventEmitterWithErrorHandling.d.ts.map +1 -1
- package/lib/eventEmitterWithErrorHandling.js +15 -2
- package/lib/eventEmitterWithErrorHandling.js.map +1 -1
- package/lib/events.d.ts +27 -3
- package/lib/events.d.ts.map +1 -1
- package/lib/events.js +26 -2
- package/lib/events.js.map +1 -1
- package/lib/fluidErrorBase.d.ts +57 -16
- package/lib/fluidErrorBase.d.ts.map +1 -1
- package/lib/fluidErrorBase.js +27 -14
- package/lib/fluidErrorBase.js.map +1 -1
- package/lib/index.d.ts +12 -11
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +11 -11
- package/lib/index.js.map +1 -1
- package/lib/logger.d.ts +269 -53
- package/lib/logger.d.ts.map +1 -1
- package/lib/logger.js +415 -131
- package/lib/logger.js.map +1 -1
- package/lib/mockLogger.d.ts +39 -12
- package/lib/mockLogger.d.ts.map +1 -1
- package/lib/mockLogger.js +106 -23
- package/lib/mockLogger.js.map +1 -1
- package/lib/sampledTelemetryHelper.d.ts +18 -12
- package/lib/sampledTelemetryHelper.d.ts.map +1 -1
- package/lib/sampledTelemetryHelper.js +26 -17
- package/lib/sampledTelemetryHelper.js.map +1 -1
- package/lib/telemetry-utils-alpha.d.mts +290 -0
- package/lib/telemetry-utils-beta.d.mts +264 -0
- package/lib/telemetry-utils-public.d.mts +264 -0
- package/lib/telemetry-utils-untrimmed.d.mts +1102 -0
- package/lib/telemetryTypes.d.ts +115 -0
- package/lib/telemetryTypes.d.ts.map +1 -0
- package/lib/telemetryTypes.js +6 -0
- package/lib/telemetryTypes.js.map +1 -0
- package/lib/thresholdCounter.d.ts +6 -5
- package/lib/thresholdCounter.d.ts.map +1 -1
- package/lib/thresholdCounter.js +4 -3
- package/lib/thresholdCounter.js.map +1 -1
- package/lib/utils.d.ts +54 -3
- package/lib/utils.d.ts.map +1 -1
- package/lib/utils.js +56 -2
- package/lib/utils.js.map +1 -1
- package/package.json +86 -57
- package/prettier.config.cjs +8 -0
- package/src/config.ts +254 -189
- package/src/error.ts +235 -0
- package/src/errorLogging.ts +440 -290
- package/src/eventEmitterWithErrorHandling.ts +26 -14
- package/src/events.ts +54 -25
- package/src/fluidErrorBase.ts +94 -46
- package/src/index.ts +76 -17
- package/src/logger.ts +972 -505
- package/src/mockLogger.ts +225 -83
- package/src/sampledTelemetryHelper.ts +136 -128
- package/src/telemetryTypes.ts +140 -0
- package/src/thresholdCounter.ts +38 -37
- package/src/utils.ts +108 -17
- package/tsconfig.esnext.json +6 -6
- package/tsconfig.json +9 -13
- package/dist/debugLogger.d.ts +0 -39
- package/dist/debugLogger.d.ts.map +0 -1
- package/dist/debugLogger.js +0 -101
- package/dist/debugLogger.js.map +0 -1
- package/dist/packageVersion.d.ts +0 -9
- package/dist/packageVersion.d.ts.map +0 -1
- package/dist/packageVersion.js +0 -12
- package/dist/packageVersion.js.map +0 -1
- package/lib/debugLogger.d.ts +0 -39
- package/lib/debugLogger.d.ts.map +0 -1
- package/lib/debugLogger.js +0 -97
- package/lib/debugLogger.js.map +0 -1
- package/lib/packageVersion.d.ts +0 -9
- package/lib/packageVersion.d.ts.map +0 -1
- package/lib/packageVersion.js +0 -9
- package/lib/packageVersion.js.map +0 -1
- package/src/debugLogger.ts +0 -126
- package/src/packageVersion.ts +0 -9
package/src/errorLogging.ts
CHANGED
|
@@ -4,88 +4,122 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
} from "@fluidframework/
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
14
|
+
hasErrorInstanceId,
|
|
15
|
+
IFluidErrorBase,
|
|
16
|
+
isFluidError,
|
|
17
|
+
isValidLegacyError,
|
|
18
18
|
} from "./fluidErrorBase";
|
|
19
|
+
import { ITelemetryLoggerExt, TelemetryEventPropertyTypeExt } from "./telemetryTypes";
|
|
19
20
|
|
|
20
|
-
/**
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
/**
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
/**
|
|
64
|
-
|
|
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
|
-
/**
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
/**
|
|
100
|
+
/**
|
|
101
|
+
* Metadata to annotate an error object when annotating or normalizing it
|
|
102
|
+
*
|
|
103
|
+
* @internal
|
|
104
|
+
*/
|
|
76
105
|
export interface IFluidErrorAnnotations {
|
|
77
|
-
|
|
78
|
-
|
|
106
|
+
/**
|
|
107
|
+
* Telemetry props to log with the error
|
|
108
|
+
*/
|
|
109
|
+
props?: ITelemetryBaseProperties;
|
|
79
110
|
}
|
|
80
111
|
|
|
81
|
-
/**
|
|
112
|
+
/**
|
|
113
|
+
* For backwards compatibility with pre-errorInstanceId valid errors
|
|
114
|
+
*/
|
|
82
115
|
function patchLegacyError(
|
|
83
|
-
|
|
116
|
+
legacyError: Omit<IFluidErrorBase, "errorInstanceId">,
|
|
84
117
|
): asserts legacyError is IFluidErrorBase {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|
|
99
|
-
|
|
134
|
+
error: unknown,
|
|
135
|
+
annotations: IFluidErrorAnnotations = {},
|
|
100
136
|
): IFluidErrorBase {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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
|
-
|
|
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
|
-
|
|
184
|
-
|
|
242
|
+
innerError: unknown,
|
|
243
|
+
newErrorFn: (message: string) => T,
|
|
185
244
|
): T {
|
|
186
|
-
|
|
187
|
-
message,
|
|
188
|
-
stack,
|
|
189
|
-
} = extractLogSafeErrorProperties(innerError, false /* sanitizeStack */);
|
|
245
|
+
const { message, stack } = extractLogSafeErrorProperties(innerError, false /* sanitizeStack */);
|
|
190
246
|
|
|
191
|
-
|
|
247
|
+
const newError = newErrorFn(message);
|
|
192
248
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
249
|
+
if (stack !== undefined) {
|
|
250
|
+
overwriteStack(newError, stack);
|
|
251
|
+
}
|
|
196
252
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
253
|
+
// Mark external errors with untrustedOrigin flag
|
|
254
|
+
if (isExternalError(innerError)) {
|
|
255
|
+
newError.addTelemetryProperties({ untrustedOrigin: 1 });
|
|
256
|
+
}
|
|
201
257
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
258
|
+
// Reuse errorInstanceId
|
|
259
|
+
if (hasErrorInstanceId(innerError)) {
|
|
260
|
+
newError.overwriteErrorInstanceId(innerError.errorInstanceId);
|
|
205
261
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
262
|
+
// For "back-compat" in the logs
|
|
263
|
+
newError.addTelemetryProperties({ innerErrorInstanceId: innerError.errorInstanceId });
|
|
264
|
+
}
|
|
209
265
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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
|
-
|
|
272
|
+
return newError;
|
|
217
273
|
}
|
|
218
274
|
|
|
219
|
-
/**
|
|
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
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
) {
|
|
225
|
-
|
|
283
|
+
innerError: unknown,
|
|
284
|
+
newErrorFn: (message: string) => T,
|
|
285
|
+
logger: ITelemetryLoggerExt,
|
|
286
|
+
): T {
|
|
287
|
+
const newError = wrapError(innerError, newErrorFn);
|
|
226
288
|
|
|
227
|
-
|
|
228
|
-
|
|
289
|
+
// This will match innerError.errorInstanceId if present (see wrapError)
|
|
290
|
+
const errorInstanceId = newError.errorInstanceId;
|
|
229
291
|
|
|
230
|
-
|
|
231
|
-
|
|
292
|
+
// For "back-compat" in the logs
|
|
293
|
+
const wrappedByErrorInstanceId = errorInstanceId;
|
|
232
294
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
295
|
+
logger.sendTelemetryEvent(
|
|
296
|
+
{
|
|
297
|
+
eventName: "WrapError",
|
|
298
|
+
errorInstanceId,
|
|
299
|
+
wrappedByErrorInstanceId,
|
|
300
|
+
},
|
|
301
|
+
innerError,
|
|
302
|
+
);
|
|
238
303
|
|
|
239
|
-
|
|
304
|
+
return newError;
|
|
240
305
|
}
|
|
241
306
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
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
|
|
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
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
265
|
-
|
|
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:
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
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
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
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
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
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
|
-
/**
|
|
380
|
-
class
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
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
|
}
|