@fluidframework/telemetry-utils 2.0.0-internal.6.1.1 → 2.0.0-internal.6.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc.js +2 -1
- package/CHANGELOG.md +59 -0
- package/README.md +4 -3
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +9 -4
- package/dist/config.js.map +1 -1
- package/dist/error.d.ts +92 -0
- package/dist/error.d.ts.map +1 -0
- package/dist/error.js +133 -0
- package/dist/error.js.map +1 -0
- package/dist/errorLogging.d.ts +34 -18
- package/dist/errorLogging.d.ts.map +1 -1
- package/dist/errorLogging.js +42 -17
- package/dist/errorLogging.js.map +1 -1
- package/dist/eventEmitterWithErrorHandling.d.ts +3 -3
- package/dist/eventEmitterWithErrorHandling.d.ts.map +1 -1
- package/dist/eventEmitterWithErrorHandling.js +10 -3
- package/dist/eventEmitterWithErrorHandling.js.map +1 -1
- package/dist/events.d.ts +1 -1
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js.map +1 -1
- package/dist/fluidErrorBase.d.ts +48 -15
- package/dist/fluidErrorBase.d.ts.map +1 -1
- package/dist/fluidErrorBase.js +18 -11
- package/dist/fluidErrorBase.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -1
- package/dist/index.js.map +1 -1
- package/dist/logger.d.ts +38 -22
- package/dist/logger.d.ts.map +1 -1
- package/dist/logger.js +68 -21
- package/dist/logger.js.map +1 -1
- package/dist/mockLogger.d.ts +17 -6
- package/dist/mockLogger.d.ts.map +1 -1
- package/dist/mockLogger.js +22 -9
- package/dist/mockLogger.js.map +1 -1
- package/dist/sampledTelemetryHelper.d.ts +8 -7
- package/dist/sampledTelemetryHelper.d.ts.map +1 -1
- package/dist/sampledTelemetryHelper.js +13 -11
- package/dist/sampledTelemetryHelper.js.map +1 -1
- package/dist/telemetryTypes.d.ts +20 -6
- package/dist/telemetryTypes.d.ts.map +1 -1
- package/dist/telemetryTypes.js.map +1 -1
- package/dist/thresholdCounter.d.ts.map +1 -1
- package/dist/thresholdCounter.js.map +1 -1
- package/dist/utils.d.ts +1 -1
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js.map +1 -1
- package/lib/config.d.ts.map +1 -1
- package/lib/config.js +9 -4
- package/lib/config.js.map +1 -1
- package/lib/error.d.ts +92 -0
- package/lib/error.d.ts.map +1 -0
- package/lib/error.js +125 -0
- package/lib/error.js.map +1 -0
- package/lib/errorLogging.d.ts +34 -18
- package/lib/errorLogging.d.ts.map +1 -1
- package/lib/errorLogging.js +42 -17
- package/lib/errorLogging.js.map +1 -1
- package/lib/eventEmitterWithErrorHandling.d.ts +3 -3
- package/lib/eventEmitterWithErrorHandling.d.ts.map +1 -1
- package/lib/eventEmitterWithErrorHandling.js +9 -2
- package/lib/eventEmitterWithErrorHandling.js.map +1 -1
- package/lib/events.d.ts +1 -1
- package/lib/events.d.ts.map +1 -1
- package/lib/events.js.map +1 -1
- package/lib/fluidErrorBase.d.ts +48 -15
- package/lib/fluidErrorBase.d.ts.map +1 -1
- package/lib/fluidErrorBase.js +18 -11
- package/lib/fluidErrorBase.js.map +1 -1
- package/lib/index.d.ts +2 -1
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +1 -0
- package/lib/index.js.map +1 -1
- package/lib/logger.d.ts +38 -22
- package/lib/logger.d.ts.map +1 -1
- package/lib/logger.js +64 -17
- package/lib/logger.js.map +1 -1
- package/lib/mockLogger.d.ts +17 -6
- package/lib/mockLogger.d.ts.map +1 -1
- package/lib/mockLogger.js +22 -9
- package/lib/mockLogger.js.map +1 -1
- package/lib/sampledTelemetryHelper.d.ts +8 -7
- package/lib/sampledTelemetryHelper.d.ts.map +1 -1
- package/lib/sampledTelemetryHelper.js +11 -9
- package/lib/sampledTelemetryHelper.js.map +1 -1
- package/lib/telemetryTypes.d.ts +20 -6
- package/lib/telemetryTypes.d.ts.map +1 -1
- package/lib/telemetryTypes.js.map +1 -1
- package/lib/thresholdCounter.d.ts.map +1 -1
- package/lib/thresholdCounter.js.map +1 -1
- package/lib/utils.d.ts +1 -1
- package/lib/utils.d.ts.map +1 -1
- package/lib/utils.js.map +1 -1
- package/package.json +15 -18
- package/src/config.ts +12 -7
- package/src/error.ts +202 -0
- package/src/errorLogging.ts +90 -52
- package/src/eventEmitterWithErrorHandling.ts +5 -3
- package/src/events.ts +3 -3
- package/src/fluidErrorBase.ts +62 -26
- package/src/index.ts +8 -0
- package/src/logger.ts +143 -45
- package/src/mockLogger.ts +33 -16
- package/src/sampledTelemetryHelper.ts +18 -14
- package/src/telemetryTypes.ts +29 -6
- package/src/thresholdCounter.ts +2 -2
- package/src/utils.ts +1 -1
package/src/config.ts
CHANGED
|
@@ -50,11 +50,12 @@ const NullConfigProvider: IConfigProviderBase = {
|
|
|
50
50
|
export const inMemoryConfigProvider = (storage: Storage | undefined): IConfigProviderBase => {
|
|
51
51
|
if (storage !== undefined && storage !== null) {
|
|
52
52
|
return new CachedConfigProvider(undefined, {
|
|
53
|
-
getRawConfig: (name: string) => {
|
|
53
|
+
getRawConfig: (name: string): ConfigTypes | undefined => {
|
|
54
54
|
try {
|
|
55
55
|
return stronglyTypedParse(storage.getItem(name) ?? undefined)?.raw;
|
|
56
|
-
} catch {
|
|
57
|
-
|
|
56
|
+
} catch {
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
58
59
|
},
|
|
59
60
|
});
|
|
60
61
|
}
|
|
@@ -104,7 +105,7 @@ function stronglyTypedParse(input: ConfigTypes): StronglyTypedValue | undefined
|
|
|
104
105
|
// holds strings
|
|
105
106
|
if (typeof input === "string") {
|
|
106
107
|
try {
|
|
107
|
-
output = JSON.parse(input);
|
|
108
|
+
output = JSON.parse(input) as ConfigTypes;
|
|
108
109
|
// we succeeded in parsing, but we don't support parsing
|
|
109
110
|
// for any object as we can't do it type safely
|
|
110
111
|
// so in this case, the default return will be string
|
|
@@ -113,7 +114,9 @@ function stronglyTypedParse(input: ConfigTypes): StronglyTypedValue | undefined
|
|
|
113
114
|
// a false sense of security by just
|
|
114
115
|
// casting.
|
|
115
116
|
defaultReturn = { raw: input, string: input };
|
|
116
|
-
} catch {
|
|
117
|
+
} catch {
|
|
118
|
+
// No-op
|
|
119
|
+
}
|
|
117
120
|
}
|
|
118
121
|
|
|
119
122
|
if (output === undefined) {
|
|
@@ -144,7 +147,9 @@ function stronglyTypedParse(input: ConfigTypes): StronglyTypedValue | undefined
|
|
|
144
147
|
return defaultReturn;
|
|
145
148
|
}
|
|
146
149
|
|
|
147
|
-
/**
|
|
150
|
+
/**
|
|
151
|
+
* `sessionStorage` is undefined in some environments such as Node and web pages with session storage disabled.
|
|
152
|
+
*/
|
|
148
153
|
const safeSessionStorage = (): Storage | undefined => {
|
|
149
154
|
// For some configurations accessing "globalThis.sessionStorage" throws
|
|
150
155
|
// "'sessionStorage' property from 'Window': Access is denied for this document" rather than returning undefined.
|
|
@@ -264,7 +269,7 @@ export function loggerToMonitoringContext<L extends ITelemetryBaseLogger = ITele
|
|
|
264
269
|
export function mixinMonitoringContext<L extends ITelemetryBaseLogger = ITelemetryLoggerExt>(
|
|
265
270
|
logger: L,
|
|
266
271
|
...configs: (IConfigProviderBase | undefined)[]
|
|
267
|
-
) {
|
|
272
|
+
): MonitoringContext<L> {
|
|
268
273
|
if (loggerIsMonitoringContext<L>(logger)) {
|
|
269
274
|
throw new Error("Logger is already a monitoring context");
|
|
270
275
|
}
|
package/src/error.ts
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Copyright (c) Microsoft Corporation and contributors. All rights reserved.
|
|
3
|
+
* Licensed under the MIT License.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
FluidErrorTypes,
|
|
8
|
+
IGenericError,
|
|
9
|
+
IErrorBase,
|
|
10
|
+
ITelemetryProperties,
|
|
11
|
+
IUsageError,
|
|
12
|
+
} from "@fluidframework/core-interfaces";
|
|
13
|
+
import { ISequencedDocumentMessage } from "@fluidframework/protocol-definitions";
|
|
14
|
+
|
|
15
|
+
import {
|
|
16
|
+
LoggingError,
|
|
17
|
+
NORMALIZED_ERROR_TYPE,
|
|
18
|
+
isExternalError,
|
|
19
|
+
normalizeError,
|
|
20
|
+
wrapError,
|
|
21
|
+
} from "./errorLogging";
|
|
22
|
+
import { IFluidErrorBase } from "./fluidErrorBase";
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Generic wrapper for an unrecognized/uncategorized error object
|
|
26
|
+
*/
|
|
27
|
+
export class GenericError extends LoggingError implements IGenericError, IFluidErrorBase {
|
|
28
|
+
readonly errorType = FluidErrorTypes.genericError;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Create a new GenericError
|
|
32
|
+
* @param message - Error message
|
|
33
|
+
* @param error - inner error object
|
|
34
|
+
* @param props - Telemetry props to include when the error is logged
|
|
35
|
+
*/
|
|
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?: ITelemetryProperties) {
|
|
39
|
+
// Don't try to log the inner error
|
|
40
|
+
super(message, props, new Set(["error"]));
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Error indicating an API is being used improperly resulting in an invalid operation.
|
|
46
|
+
*/
|
|
47
|
+
export class UsageError extends LoggingError implements IUsageError, IFluidErrorBase {
|
|
48
|
+
readonly errorType = FluidErrorTypes.usageError;
|
|
49
|
+
|
|
50
|
+
constructor(message: string, props?: ITelemetryProperties) {
|
|
51
|
+
super(message, { ...props, usageError: true });
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* DataCorruptionError indicates that we encountered definitive evidence that the data at rest
|
|
57
|
+
* backing this container is corrupted, and this container would never be expected to load properly again
|
|
58
|
+
*/
|
|
59
|
+
export class DataCorruptionError extends LoggingError implements IErrorBase, IFluidErrorBase {
|
|
60
|
+
readonly errorType = FluidErrorTypes.dataCorruptionError;
|
|
61
|
+
readonly canRetry = false;
|
|
62
|
+
|
|
63
|
+
constructor(message: string, props: ITelemetryProperties) {
|
|
64
|
+
super(message, { ...props, dataProcessingError: 1 });
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Indicates we hit a fatal error while processing incoming data from the Fluid Service.
|
|
70
|
+
*
|
|
71
|
+
* @remarks
|
|
72
|
+
*
|
|
73
|
+
* The error will often originate in the dataStore or DDS implementation that is responding to incoming changes.
|
|
74
|
+
* This differs from {@link DataCorruptionError} in that this may be a transient error that will not repro in another
|
|
75
|
+
* client or session.
|
|
76
|
+
*/
|
|
77
|
+
export class DataProcessingError extends LoggingError implements IErrorBase, IFluidErrorBase {
|
|
78
|
+
/**
|
|
79
|
+
* {@inheritDoc IFluidErrorBase.errorType}
|
|
80
|
+
*/
|
|
81
|
+
public readonly errorType = FluidErrorTypes.dataProcessingError;
|
|
82
|
+
|
|
83
|
+
public readonly canRetry = false;
|
|
84
|
+
|
|
85
|
+
private constructor(errorMessage: string) {
|
|
86
|
+
super(errorMessage);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Create a new `DataProcessingError` detected and raised within the Fluid Framework.
|
|
91
|
+
*/
|
|
92
|
+
public static create(
|
|
93
|
+
errorMessage: string,
|
|
94
|
+
dataProcessingCodepath: string,
|
|
95
|
+
sequencedMessage?: ISequencedDocumentMessage,
|
|
96
|
+
props: ITelemetryProperties = {},
|
|
97
|
+
): IFluidErrorBase {
|
|
98
|
+
const dataProcessingError = DataProcessingError.wrapIfUnrecognized(
|
|
99
|
+
errorMessage,
|
|
100
|
+
dataProcessingCodepath,
|
|
101
|
+
sequencedMessage,
|
|
102
|
+
);
|
|
103
|
+
dataProcessingError.addTelemetryProperties(props);
|
|
104
|
+
|
|
105
|
+
return dataProcessingError;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Wrap the given error in a `DataProcessingError`, unless the error is already of a known type
|
|
110
|
+
* with the exception of a normalized {@link LoggingError}, which will still be wrapped.
|
|
111
|
+
*
|
|
112
|
+
* In either case, the error will have some relevant properties added for telemetry.
|
|
113
|
+
*
|
|
114
|
+
* @remarks
|
|
115
|
+
*
|
|
116
|
+
* We wrap conditionally since known error types represent well-understood failure modes, and ideally
|
|
117
|
+
* one day we will move away from throwing these errors but rather we'll return them.
|
|
118
|
+
* But an unrecognized error needs to be classified as `DataProcessingError`.
|
|
119
|
+
*
|
|
120
|
+
* @param originalError - The error to be converted.
|
|
121
|
+
* @param dataProcessingCodepath - Which code-path failed while processing data.
|
|
122
|
+
* @param messageLike - Message to include info about via telemetry props.
|
|
123
|
+
*
|
|
124
|
+
* @returns Either a new `DataProcessingError`, or (if wrapping is deemed unnecessary) the given error.
|
|
125
|
+
*/
|
|
126
|
+
public static wrapIfUnrecognized(
|
|
127
|
+
originalError: unknown,
|
|
128
|
+
dataProcessingCodepath: string,
|
|
129
|
+
messageLike?: Partial<
|
|
130
|
+
Pick<
|
|
131
|
+
ISequencedDocumentMessage,
|
|
132
|
+
| "clientId"
|
|
133
|
+
| "sequenceNumber"
|
|
134
|
+
| "clientSequenceNumber"
|
|
135
|
+
| "referenceSequenceNumber"
|
|
136
|
+
| "minimumSequenceNumber"
|
|
137
|
+
| "timestamp"
|
|
138
|
+
>
|
|
139
|
+
>,
|
|
140
|
+
): IFluidErrorBase {
|
|
141
|
+
const props = {
|
|
142
|
+
dataProcessingError: 1,
|
|
143
|
+
dataProcessingCodepath,
|
|
144
|
+
...(messageLike === undefined
|
|
145
|
+
? undefined
|
|
146
|
+
: extractSafePropertiesFromMessage(messageLike)),
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
const normalizedError = normalizeError(originalError, { props });
|
|
150
|
+
// Note that other errors may have the NORMALIZED_ERROR_TYPE errorType,
|
|
151
|
+
// but if so they are still suitable to be wrapped as DataProcessingError.
|
|
152
|
+
if (
|
|
153
|
+
isExternalError(normalizedError) ||
|
|
154
|
+
normalizedError.errorType === NORMALIZED_ERROR_TYPE
|
|
155
|
+
) {
|
|
156
|
+
// Create a new DataProcessingError to wrap this external error
|
|
157
|
+
const dataProcessingError = wrapError(
|
|
158
|
+
normalizedError,
|
|
159
|
+
(message: string) => new DataProcessingError(message),
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
// Copy over the props above and any others added to this error since first being normalized
|
|
163
|
+
dataProcessingError.addTelemetryProperties(normalizedError.getTelemetryProperties());
|
|
164
|
+
|
|
165
|
+
return dataProcessingError;
|
|
166
|
+
}
|
|
167
|
+
return normalizedError;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Extracts specific properties from the provided message that we know are safe to log.
|
|
173
|
+
*
|
|
174
|
+
* @param messageLike - Message to include info about via telemetry props.
|
|
175
|
+
*/
|
|
176
|
+
export const extractSafePropertiesFromMessage = (
|
|
177
|
+
messageLike: Partial<
|
|
178
|
+
Pick<
|
|
179
|
+
ISequencedDocumentMessage,
|
|
180
|
+
| "clientId"
|
|
181
|
+
| "sequenceNumber"
|
|
182
|
+
| "clientSequenceNumber"
|
|
183
|
+
| "referenceSequenceNumber"
|
|
184
|
+
| "minimumSequenceNumber"
|
|
185
|
+
| "timestamp"
|
|
186
|
+
>
|
|
187
|
+
>,
|
|
188
|
+
): {
|
|
189
|
+
messageClientId: string | undefined;
|
|
190
|
+
messageSequenceNumber: number | undefined;
|
|
191
|
+
messageClientSequenceNumber: number | undefined;
|
|
192
|
+
messageReferenceSequenceNumber: number | undefined;
|
|
193
|
+
messageMinimumSequenceNumber: number | undefined;
|
|
194
|
+
messageTimestamp: number | undefined;
|
|
195
|
+
} => ({
|
|
196
|
+
messageClientId: messageLike.clientId === null ? "null" : messageLike.clientId,
|
|
197
|
+
messageSequenceNumber: messageLike.sequenceNumber,
|
|
198
|
+
messageClientSequenceNumber: messageLike.clientSequenceNumber,
|
|
199
|
+
messageReferenceSequenceNumber: messageLike.referenceSequenceNumber,
|
|
200
|
+
messageMinimumSequenceNumber: messageLike.minimumSequenceNumber,
|
|
201
|
+
messageTimestamp: messageLike.timestamp,
|
|
202
|
+
});
|
package/src/errorLogging.ts
CHANGED
|
@@ -5,9 +5,9 @@
|
|
|
5
5
|
|
|
6
6
|
import {
|
|
7
7
|
ILoggingError,
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
ITelemetryBaseProperties,
|
|
9
|
+
TelemetryBaseEventPropertyType,
|
|
10
|
+
Tagged,
|
|
11
11
|
} from "@fluidframework/core-interfaces";
|
|
12
12
|
import { v4 as uuid } from "uuid";
|
|
13
13
|
import {
|
|
@@ -16,20 +16,27 @@ import {
|
|
|
16
16
|
isFluidError,
|
|
17
17
|
isValidLegacyError,
|
|
18
18
|
} from "./fluidErrorBase";
|
|
19
|
-
import {
|
|
20
|
-
ITaggedTelemetryPropertyTypeExt,
|
|
21
|
-
ITelemetryLoggerExt,
|
|
22
|
-
TelemetryEventPropertyTypeExt,
|
|
23
|
-
} from "./telemetryTypes";
|
|
19
|
+
import { ITelemetryLoggerExt, TelemetryEventPropertyTypeExt } from "./telemetryTypes";
|
|
24
20
|
|
|
25
|
-
/**
|
|
26
|
-
|
|
21
|
+
/**
|
|
22
|
+
* Determines if the provided value is an object but neither null nor an array.
|
|
23
|
+
*/
|
|
24
|
+
const isRegularObject = (value: unknown): boolean => {
|
|
27
25
|
return value !== null && !Array.isArray(value) && typeof value === "object";
|
|
28
26
|
};
|
|
29
27
|
|
|
30
|
-
/**
|
|
31
|
-
|
|
32
|
-
|
|
28
|
+
/**
|
|
29
|
+
* Inspect the given error for common "safe" props and return them.
|
|
30
|
+
*/
|
|
31
|
+
export function extractLogSafeErrorProperties(
|
|
32
|
+
error: unknown,
|
|
33
|
+
sanitizeStack: boolean,
|
|
34
|
+
): {
|
|
35
|
+
message: string;
|
|
36
|
+
errorType?: string | undefined;
|
|
37
|
+
stack?: string | undefined;
|
|
38
|
+
} {
|
|
39
|
+
const removeMessageFromStack = (stack: string, errorName?: string): string => {
|
|
33
40
|
if (!sanitizeStack) {
|
|
34
41
|
return stack;
|
|
35
42
|
}
|
|
@@ -41,14 +48,17 @@ export function extractLogSafeErrorProperties(error: any, sanitizeStack: boolean
|
|
|
41
48
|
return stackFrames.join("\n");
|
|
42
49
|
};
|
|
43
50
|
|
|
44
|
-
const message =
|
|
51
|
+
const message =
|
|
52
|
+
typeof (error as Partial<Error>)?.message === "string"
|
|
53
|
+
? (error as Error).message
|
|
54
|
+
: String(error);
|
|
45
55
|
|
|
46
56
|
const safeProps: { message: string; errorType?: string; stack?: string } = {
|
|
47
57
|
message,
|
|
48
58
|
};
|
|
49
59
|
|
|
50
60
|
if (isRegularObject(error)) {
|
|
51
|
-
const { errorType, stack, name } = error
|
|
61
|
+
const { errorType, stack, name } = error as Partial<IFluidErrorBase>;
|
|
52
62
|
|
|
53
63
|
if (typeof errorType === "string") {
|
|
54
64
|
safeProps.errorType = errorType;
|
|
@@ -63,12 +73,19 @@ export function extractLogSafeErrorProperties(error: any, sanitizeStack: boolean
|
|
|
63
73
|
return safeProps;
|
|
64
74
|
}
|
|
65
75
|
|
|
66
|
-
/**
|
|
67
|
-
|
|
68
|
-
|
|
76
|
+
/**
|
|
77
|
+
* type guard for ILoggingError interface
|
|
78
|
+
*/
|
|
79
|
+
export const isILoggingError = (x: unknown): x is ILoggingError =>
|
|
80
|
+
typeof (x as Partial<ILoggingError>)?.getTelemetryProperties === "function";
|
|
69
81
|
|
|
70
|
-
/**
|
|
71
|
-
|
|
82
|
+
/**
|
|
83
|
+
* Copy props from source onto target, but do not overwrite an existing prop that matches
|
|
84
|
+
*/
|
|
85
|
+
function copyProps(
|
|
86
|
+
target: ITelemetryBaseProperties | LoggingError,
|
|
87
|
+
source: ITelemetryBaseProperties,
|
|
88
|
+
): void {
|
|
72
89
|
for (const key of Object.keys(source)) {
|
|
73
90
|
if (target[key] === undefined) {
|
|
74
91
|
target[key] = source[key];
|
|
@@ -76,16 +93,23 @@ function copyProps(target: ITelemetryProperties | LoggingError, source: ITelemet
|
|
|
76
93
|
}
|
|
77
94
|
}
|
|
78
95
|
|
|
79
|
-
/**
|
|
96
|
+
/**
|
|
97
|
+
* Metadata to annotate an error object when annotating or normalizing it
|
|
98
|
+
*/
|
|
80
99
|
export interface IFluidErrorAnnotations {
|
|
81
|
-
/**
|
|
82
|
-
|
|
100
|
+
/**
|
|
101
|
+
* Telemetry props to log with the error
|
|
102
|
+
*/
|
|
103
|
+
props?: ITelemetryBaseProperties;
|
|
83
104
|
}
|
|
84
105
|
|
|
85
|
-
/**
|
|
106
|
+
/**
|
|
107
|
+
* For backwards compatibility with pre-errorInstanceId valid errors
|
|
108
|
+
*/
|
|
86
109
|
function patchLegacyError(
|
|
87
110
|
legacyError: Omit<IFluidErrorBase, "errorInstanceId">,
|
|
88
111
|
): asserts legacyError is IFluidErrorBase {
|
|
112
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-explicit-any
|
|
89
113
|
const patchMe: { -readonly [P in "errorInstanceId"]?: IFluidErrorBase[P] } = legacyError as any;
|
|
90
114
|
if (patchMe.errorInstanceId === undefined) {
|
|
91
115
|
patchMe.errorInstanceId = uuid();
|
|
@@ -180,8 +204,8 @@ export function generateErrorWithStack(): Error {
|
|
|
180
204
|
|
|
181
205
|
try {
|
|
182
206
|
throw err;
|
|
183
|
-
} catch (
|
|
184
|
-
return
|
|
207
|
+
} catch (error) {
|
|
208
|
+
return error as Error;
|
|
185
209
|
}
|
|
186
210
|
}
|
|
187
211
|
|
|
@@ -230,12 +254,16 @@ export function wrapError<T extends LoggingError>(
|
|
|
230
254
|
return newError;
|
|
231
255
|
}
|
|
232
256
|
|
|
233
|
-
/**
|
|
257
|
+
/**
|
|
258
|
+
* The same as wrapError, but also logs the innerError, including the wrapping error's instance ID.
|
|
259
|
+
*
|
|
260
|
+
* @typeParam T - The kind of wrapper error to create.
|
|
261
|
+
*/
|
|
234
262
|
export function wrapErrorAndLog<T extends LoggingError>(
|
|
235
263
|
innerError: unknown,
|
|
236
264
|
newErrorFn: (message: string) => T,
|
|
237
265
|
logger: ITelemetryLoggerExt,
|
|
238
|
-
) {
|
|
266
|
+
): T {
|
|
239
267
|
const newError = wrapError(innerError, newErrorFn);
|
|
240
268
|
|
|
241
269
|
// This will match innerError.errorInstanceId if present (see wrapError)
|
|
@@ -256,11 +284,11 @@ export function wrapErrorAndLog<T extends LoggingError>(
|
|
|
256
284
|
return newError;
|
|
257
285
|
}
|
|
258
286
|
|
|
259
|
-
function overwriteStack(error: IFluidErrorBase | LoggingError, stack: string) {
|
|
287
|
+
function overwriteStack(error: IFluidErrorBase | LoggingError, stack: string): void {
|
|
260
288
|
// supposedly setting stack on an Error can throw.
|
|
261
289
|
try {
|
|
262
290
|
Object.assign(error, { stack });
|
|
263
|
-
} catch
|
|
291
|
+
} catch {
|
|
264
292
|
error.addTelemetryProperties({ stack2: stack });
|
|
265
293
|
}
|
|
266
294
|
}
|
|
@@ -270,26 +298,26 @@ function overwriteStack(error: IFluidErrorBase | LoggingError, stack: string) {
|
|
|
270
298
|
* False for any error we created and raised within the FF codebase via LoggingError base class,
|
|
271
299
|
* or wrapped in a well-known error type
|
|
272
300
|
*/
|
|
273
|
-
export function isExternalError(
|
|
301
|
+
export function isExternalError(error: unknown): boolean {
|
|
274
302
|
// LoggingErrors are an internal FF error type. However, an external error can be converted
|
|
275
303
|
// into a LoggingError if it is normalized. In this case we must use the untrustedOrigin flag to
|
|
276
304
|
// determine whether the original error was infact external.
|
|
277
|
-
if (LoggingError.typeCheck(
|
|
278
|
-
if ((
|
|
279
|
-
return
|
|
305
|
+
if (LoggingError.typeCheck(error)) {
|
|
306
|
+
if ((error as NormalizedLoggingError).errorType === NORMALIZED_ERROR_TYPE) {
|
|
307
|
+
return error.getTelemetryProperties().untrustedOrigin === 1;
|
|
280
308
|
}
|
|
281
309
|
return false;
|
|
282
310
|
}
|
|
283
|
-
return !isValidLegacyError(
|
|
311
|
+
return !isValidLegacyError(error);
|
|
284
312
|
}
|
|
285
313
|
|
|
286
314
|
/**
|
|
287
315
|
* Type guard to identify if a particular telemetry property appears to be a tagged telemetry property
|
|
288
316
|
*/
|
|
289
317
|
export function isTaggedTelemetryPropertyValue(
|
|
290
|
-
x:
|
|
291
|
-
): x is
|
|
292
|
-
return typeof (x as
|
|
318
|
+
x: Tagged<TelemetryEventPropertyTypeExt> | TelemetryEventPropertyTypeExt,
|
|
319
|
+
): x is Tagged<TelemetryEventPropertyTypeExt> {
|
|
320
|
+
return typeof (x as Partial<Tagged<unknown>>)?.tag === "string";
|
|
293
321
|
}
|
|
294
322
|
|
|
295
323
|
/**
|
|
@@ -298,7 +326,7 @@ export function isTaggedTelemetryPropertyValue(
|
|
|
298
326
|
* @returns - as-is if x is primitive. returns stringified if x is an array of primitive.
|
|
299
327
|
* otherwise returns null since this is what we support at the moment.
|
|
300
328
|
*/
|
|
301
|
-
function filterValidTelemetryProps(x:
|
|
329
|
+
function filterValidTelemetryProps(x: unknown, key: string): TelemetryBaseEventPropertyType {
|
|
302
330
|
if (Array.isArray(x) && x.every((val) => isTelemetryEventPropertyValue(val))) {
|
|
303
331
|
return JSON.stringify(x);
|
|
304
332
|
}
|
|
@@ -311,7 +339,7 @@ function filterValidTelemetryProps(x: any, key: string): TelemetryEventPropertyT
|
|
|
311
339
|
}
|
|
312
340
|
|
|
313
341
|
// checking type of x, returns false if x is null
|
|
314
|
-
function isTelemetryEventPropertyValue(x:
|
|
342
|
+
function isTelemetryEventPropertyValue(x: unknown): x is TelemetryBaseEventPropertyType {
|
|
315
343
|
switch (typeof x) {
|
|
316
344
|
case "string":
|
|
317
345
|
case "number":
|
|
@@ -325,13 +353,15 @@ function isTelemetryEventPropertyValue(x: any): x is TelemetryEventPropertyType
|
|
|
325
353
|
/**
|
|
326
354
|
* Walk an object's enumerable properties to find those fit for telemetry.
|
|
327
355
|
*/
|
|
328
|
-
function getValidTelemetryProps(obj:
|
|
329
|
-
const props:
|
|
356
|
+
function getValidTelemetryProps(obj: object, keysToOmit: Set<string>): ITelemetryBaseProperties {
|
|
357
|
+
const props: ITelemetryBaseProperties = {};
|
|
330
358
|
for (const key of Object.keys(obj)) {
|
|
331
359
|
if (keysToOmit.has(key)) {
|
|
332
360
|
continue;
|
|
333
361
|
}
|
|
334
|
-
const val = obj[key]
|
|
362
|
+
const val = obj[key] as
|
|
363
|
+
| TelemetryEventPropertyTypeExt
|
|
364
|
+
| Tagged<TelemetryEventPropertyTypeExt>;
|
|
335
365
|
|
|
336
366
|
// ensure only valid props get logged, since props of logging error could be in any shape
|
|
337
367
|
if (isTaggedTelemetryPropertyValue(val)) {
|
|
@@ -353,9 +383,11 @@ function getValidTelemetryProps(obj: any, keysToOmit: Set<string>): ITelemetryPr
|
|
|
353
383
|
* Not ideal, as will cut values that are not necessarily circular references.
|
|
354
384
|
* Could be improved by implementing Node's util.inspect() for browser (minus all the coloring code)
|
|
355
385
|
*/
|
|
356
|
-
|
|
386
|
+
// TODO: Use `unknown` instead (API breaking change)
|
|
387
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
388
|
+
export const getCircularReplacer = (): ((key: string, value: unknown) => any) => {
|
|
357
389
|
const seen = new WeakSet();
|
|
358
|
-
return (key: string, value:
|
|
390
|
+
return (key: string, value: unknown): any => {
|
|
359
391
|
if (typeof value === "object" && value !== null) {
|
|
360
392
|
if (seen.has(value)) {
|
|
361
393
|
return "<removed/circular>";
|
|
@@ -365,6 +397,7 @@ export const getCircularReplacer = () => {
|
|
|
365
397
|
return value;
|
|
366
398
|
};
|
|
367
399
|
};
|
|
400
|
+
/* eslint-enable @typescript-eslint/no-explicit-any */
|
|
368
401
|
|
|
369
402
|
/**
|
|
370
403
|
* Base class for "trusted" errors we create, whose properties can generally be logged to telemetry safely.
|
|
@@ -378,15 +411,18 @@ export class LoggingError
|
|
|
378
411
|
implements ILoggingError, Omit<IFluidErrorBase, "errorType">
|
|
379
412
|
{
|
|
380
413
|
private _errorInstanceId = uuid();
|
|
381
|
-
get errorInstanceId() {
|
|
414
|
+
get errorInstanceId(): string {
|
|
382
415
|
return this._errorInstanceId;
|
|
383
416
|
}
|
|
384
|
-
overwriteErrorInstanceId(id: string) {
|
|
417
|
+
overwriteErrorInstanceId(id: string): void {
|
|
385
418
|
this._errorInstanceId = id;
|
|
386
419
|
}
|
|
387
420
|
|
|
388
|
-
/**
|
|
421
|
+
/**
|
|
422
|
+
* Backwards compatibility to appease {@link isFluidError} in old code that may handle this error.
|
|
423
|
+
*/
|
|
389
424
|
// @ts-expect-error - This field shouldn't be referenced in the current version, but needs to exist at runtime.
|
|
425
|
+
// eslint-disable-next-line @typescript-eslint/prefer-as-const
|
|
390
426
|
private readonly fluidErrorCode: "-" = "-";
|
|
391
427
|
|
|
392
428
|
/**
|
|
@@ -397,7 +433,7 @@ export class LoggingError
|
|
|
397
433
|
*/
|
|
398
434
|
constructor(
|
|
399
435
|
message: string,
|
|
400
|
-
props?:
|
|
436
|
+
props?: ITelemetryBaseProperties,
|
|
401
437
|
private readonly omitPropsFromLogging: Set<string> = new Set(),
|
|
402
438
|
) {
|
|
403
439
|
super(message);
|
|
@@ -430,14 +466,14 @@ export class LoggingError
|
|
|
430
466
|
/**
|
|
431
467
|
* Add additional properties to be logged
|
|
432
468
|
*/
|
|
433
|
-
public addTelemetryProperties(props:
|
|
469
|
+
public addTelemetryProperties(props: ITelemetryBaseProperties): void {
|
|
434
470
|
copyProps(this, props);
|
|
435
471
|
}
|
|
436
472
|
|
|
437
473
|
/**
|
|
438
474
|
* Get all properties fit to be logged to telemetry for this error
|
|
439
475
|
*/
|
|
440
|
-
public getTelemetryProperties():
|
|
476
|
+
public getTelemetryProperties(): ITelemetryBaseProperties {
|
|
441
477
|
const taggableProps = getValidTelemetryProps(this, this.omitPropsFromLogging);
|
|
442
478
|
// Include non-enumerable props that are not returned by getValidTelemetryProps
|
|
443
479
|
return {
|
|
@@ -449,7 +485,9 @@ export class LoggingError
|
|
|
449
485
|
}
|
|
450
486
|
}
|
|
451
487
|
|
|
452
|
-
/**
|
|
488
|
+
/**
|
|
489
|
+
* The Error class used when normalizing an external error
|
|
490
|
+
*/
|
|
453
491
|
export const NORMALIZED_ERROR_TYPE = "genericError";
|
|
454
492
|
class NormalizedLoggingError extends LoggingError {
|
|
455
493
|
// errorType "genericError" is used as a default value throughout the code.
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* Copyright (c) Microsoft Corporation and contributors. All rights reserved.
|
|
3
3
|
* Licensed under the MIT License.
|
|
4
4
|
*/
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
5
|
+
import { TypedEventEmitter, EventEmitterEventType } from "@fluid-internal/client-utils";
|
|
6
|
+
import { IEvent } from "@fluidframework/core-interfaces";
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* Event Emitter helper class
|
|
@@ -14,12 +14,14 @@ export class EventEmitterWithErrorHandling<
|
|
|
14
14
|
TEvent extends IEvent = IEvent,
|
|
15
15
|
> extends TypedEventEmitter<TEvent> {
|
|
16
16
|
constructor(
|
|
17
|
+
// TODO: use `unknown` instead (breaking API change)
|
|
18
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
17
19
|
private readonly errorHandler: (eventName: EventEmitterEventType, error: any) => void,
|
|
18
20
|
) {
|
|
19
21
|
super();
|
|
20
22
|
}
|
|
21
23
|
|
|
22
|
-
public emit(event: EventEmitterEventType, ...args:
|
|
24
|
+
public emit(event: EventEmitterEventType, ...args: unknown[]): boolean {
|
|
23
25
|
try {
|
|
24
26
|
return super.emit(event, ...args);
|
|
25
27
|
} catch (error) {
|
package/src/events.ts
CHANGED
|
@@ -13,8 +13,8 @@ export function safeRaiseEvent(
|
|
|
13
13
|
emitter: EventEmitter,
|
|
14
14
|
logger: ITelemetryLoggerExt,
|
|
15
15
|
event: string,
|
|
16
|
-
...args
|
|
17
|
-
) {
|
|
16
|
+
...args: unknown[]
|
|
17
|
+
): void {
|
|
18
18
|
try {
|
|
19
19
|
emitter.emit(event, ...args);
|
|
20
20
|
} catch (error) {
|
|
@@ -36,7 +36,7 @@ export function raiseConnectedEvent(
|
|
|
36
36
|
connected: boolean,
|
|
37
37
|
clientId?: string,
|
|
38
38
|
disconnectedReason?: string,
|
|
39
|
-
) {
|
|
39
|
+
): void {
|
|
40
40
|
try {
|
|
41
41
|
if (connected) {
|
|
42
42
|
emitter.emit(connectedEventName, clientId);
|