@posthog/core 1.25.3 → 1.26.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/dist/error-tracking/exception-steps.d.ts +38 -0
- package/dist/error-tracking/exception-steps.d.ts.map +1 -0
- package/dist/error-tracking/exception-steps.js +187 -0
- package/dist/error-tracking/exception-steps.mjs +138 -0
- package/dist/error-tracking/index.d.ts +1 -0
- package/dist/error-tracking/index.d.ts.map +1 -1
- package/dist/error-tracking/index.js +9 -0
- package/dist/error-tracking/index.mjs +1 -0
- package/package.json +1 -1
- package/src/error-tracking/exception-steps.spec.ts +90 -0
- package/src/error-tracking/exception-steps.ts +225 -0
- package/src/error-tracking/index.ts +1 -0
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export declare const EXCEPTION_STEP_INTERNAL_FIELDS: {
|
|
2
|
+
readonly MESSAGE: "$message";
|
|
3
|
+
readonly TIMESTAMP: "$timestamp";
|
|
4
|
+
};
|
|
5
|
+
export type ExceptionStep = {
|
|
6
|
+
[EXCEPTION_STEP_INTERNAL_FIELDS.MESSAGE]: string;
|
|
7
|
+
[EXCEPTION_STEP_INTERNAL_FIELDS.TIMESTAMP]: string | number;
|
|
8
|
+
[key: string]: unknown;
|
|
9
|
+
};
|
|
10
|
+
/** NOTE: This type is also defined in `@posthog/types` (posthog-config.ts). Keep both in sync. */
|
|
11
|
+
export type ExceptionStepsConfig = {
|
|
12
|
+
enabled?: boolean;
|
|
13
|
+
max_bytes?: number;
|
|
14
|
+
};
|
|
15
|
+
export type ResolvedExceptionStepsConfig = {
|
|
16
|
+
enabled: boolean;
|
|
17
|
+
max_bytes: number;
|
|
18
|
+
};
|
|
19
|
+
export declare const DEFAULT_EXCEPTION_STEPS_CONFIG: ResolvedExceptionStepsConfig;
|
|
20
|
+
export declare function resolveExceptionStepsConfig(config?: ExceptionStepsConfig | null): ResolvedExceptionStepsConfig;
|
|
21
|
+
export declare function stripReservedExceptionStepFields(properties?: Record<string, unknown> | null): {
|
|
22
|
+
sanitizedProperties: Record<string, unknown>;
|
|
23
|
+
droppedKeys: string[];
|
|
24
|
+
};
|
|
25
|
+
export declare class ExceptionStepsBuffer {
|
|
26
|
+
private _entries;
|
|
27
|
+
private _totalBytes;
|
|
28
|
+
private _config;
|
|
29
|
+
constructor(config?: ExceptionStepsConfig | null);
|
|
30
|
+
setConfig(config?: ExceptionStepsConfig | null): void;
|
|
31
|
+
add(step: ExceptionStep): void;
|
|
32
|
+
getAttachable(): ExceptionStep[];
|
|
33
|
+
clear(): void;
|
|
34
|
+
size(): number;
|
|
35
|
+
private _trimToMaxBytes;
|
|
36
|
+
}
|
|
37
|
+
export declare function getUtf8ByteLength(value: string): number;
|
|
38
|
+
//# sourceMappingURL=exception-steps.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"exception-steps.d.ts","sourceRoot":"","sources":["../../src/error-tracking/exception-steps.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,8BAA8B;;;CAGjC,CAAA;AAOV,MAAM,MAAM,aAAa,GAAG;IAC1B,CAAC,8BAA8B,CAAC,OAAO,CAAC,EAAE,MAAM,CAAA;IAChD,CAAC,8BAA8B,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;IAC3D,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;CACvB,CAAA;AAED,kGAAkG;AAClG,MAAM,MAAM,oBAAoB,GAAG;IACjC,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB,CAAA;AAED,MAAM,MAAM,4BAA4B,GAAG;IACzC,OAAO,EAAE,OAAO,CAAA;IAChB,SAAS,EAAE,MAAM,CAAA;CAClB,CAAA;AAED,eAAO,MAAM,8BAA8B,EAAE,4BAG5C,CAAA;AAED,wBAAgB,2BAA2B,CAAC,MAAM,CAAC,EAAE,oBAAoB,GAAG,IAAI,GAAG,4BAA4B,CAS9G;AAED,wBAAgB,gCAAgC,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,GAAG;IAC7F,mBAAmB,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAC5C,WAAW,EAAE,MAAM,EAAE,CAAA;CACtB,CAmBA;AAED,qBAAa,oBAAoB;IAC/B,OAAO,CAAC,QAAQ,CAA+C;IAC/D,OAAO,CAAC,WAAW,CAAY;IAC/B,OAAO,CAAC,OAAO,CAA8B;gBAEjC,MAAM,CAAC,EAAE,oBAAoB,GAAG,IAAI;IAIzC,SAAS,CAAC,MAAM,CAAC,EAAE,oBAAoB,GAAG,IAAI,GAAG,IAAI;IAKrD,GAAG,CAAC,IAAI,EAAE,aAAa,GAAG,IAAI;IAgB9B,aAAa,IAAI,aAAa,EAAE;IAIhC,KAAK,IAAI,IAAI;IAKb,IAAI,IAAI,MAAM;IAIrB,OAAO,CAAC,eAAe;CAQxB;AAuFD,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAiBvD"}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __webpack_require__ = {};
|
|
3
|
+
(()=>{
|
|
4
|
+
__webpack_require__.d = (exports1, definition)=>{
|
|
5
|
+
for(var key in definition)if (__webpack_require__.o(definition, key) && !__webpack_require__.o(exports1, key)) Object.defineProperty(exports1, key, {
|
|
6
|
+
enumerable: true,
|
|
7
|
+
get: definition[key]
|
|
8
|
+
});
|
|
9
|
+
};
|
|
10
|
+
})();
|
|
11
|
+
(()=>{
|
|
12
|
+
__webpack_require__.o = (obj, prop)=>Object.prototype.hasOwnProperty.call(obj, prop);
|
|
13
|
+
})();
|
|
14
|
+
(()=>{
|
|
15
|
+
__webpack_require__.r = (exports1)=>{
|
|
16
|
+
if ('undefined' != typeof Symbol && Symbol.toStringTag) Object.defineProperty(exports1, Symbol.toStringTag, {
|
|
17
|
+
value: 'Module'
|
|
18
|
+
});
|
|
19
|
+
Object.defineProperty(exports1, '__esModule', {
|
|
20
|
+
value: true
|
|
21
|
+
});
|
|
22
|
+
};
|
|
23
|
+
})();
|
|
24
|
+
var __webpack_exports__ = {};
|
|
25
|
+
__webpack_require__.r(__webpack_exports__);
|
|
26
|
+
__webpack_require__.d(__webpack_exports__, {
|
|
27
|
+
stripReservedExceptionStepFields: ()=>stripReservedExceptionStepFields,
|
|
28
|
+
ExceptionStepsBuffer: ()=>ExceptionStepsBuffer,
|
|
29
|
+
EXCEPTION_STEP_INTERNAL_FIELDS: ()=>EXCEPTION_STEP_INTERNAL_FIELDS,
|
|
30
|
+
resolveExceptionStepsConfig: ()=>resolveExceptionStepsConfig,
|
|
31
|
+
getUtf8ByteLength: ()=>getUtf8ByteLength,
|
|
32
|
+
DEFAULT_EXCEPTION_STEPS_CONFIG: ()=>DEFAULT_EXCEPTION_STEPS_CONFIG
|
|
33
|
+
});
|
|
34
|
+
const index_js_namespaceObject = require("../utils/index.js");
|
|
35
|
+
const EXCEPTION_STEP_INTERNAL_FIELDS = {
|
|
36
|
+
MESSAGE: '$message',
|
|
37
|
+
TIMESTAMP: '$timestamp'
|
|
38
|
+
};
|
|
39
|
+
const RESERVED_EXCEPTION_STEP_KEYS = new Set([
|
|
40
|
+
EXCEPTION_STEP_INTERNAL_FIELDS.MESSAGE,
|
|
41
|
+
EXCEPTION_STEP_INTERNAL_FIELDS.TIMESTAMP
|
|
42
|
+
]);
|
|
43
|
+
const DEFAULT_EXCEPTION_STEPS_CONFIG = {
|
|
44
|
+
enabled: true,
|
|
45
|
+
max_bytes: 32768
|
|
46
|
+
};
|
|
47
|
+
function resolveExceptionStepsConfig(config) {
|
|
48
|
+
if (!config) return {
|
|
49
|
+
...DEFAULT_EXCEPTION_STEPS_CONFIG
|
|
50
|
+
};
|
|
51
|
+
return {
|
|
52
|
+
enabled: config.enabled ?? DEFAULT_EXCEPTION_STEPS_CONFIG.enabled,
|
|
53
|
+
max_bytes: normalizePositiveInteger(config.max_bytes, DEFAULT_EXCEPTION_STEPS_CONFIG.max_bytes)
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
function stripReservedExceptionStepFields(properties) {
|
|
57
|
+
if (!properties) return {
|
|
58
|
+
sanitizedProperties: {},
|
|
59
|
+
droppedKeys: []
|
|
60
|
+
};
|
|
61
|
+
const droppedKeys = [];
|
|
62
|
+
const sanitizedProperties = Object.keys(properties).reduce((acc, key)=>{
|
|
63
|
+
if (RESERVED_EXCEPTION_STEP_KEYS.has(key)) {
|
|
64
|
+
droppedKeys.push(key);
|
|
65
|
+
return acc;
|
|
66
|
+
}
|
|
67
|
+
acc[key] = properties[key];
|
|
68
|
+
return acc;
|
|
69
|
+
}, {});
|
|
70
|
+
return {
|
|
71
|
+
sanitizedProperties,
|
|
72
|
+
droppedKeys
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
class ExceptionStepsBuffer {
|
|
76
|
+
constructor(config){
|
|
77
|
+
this._entries = [];
|
|
78
|
+
this._totalBytes = 0;
|
|
79
|
+
this._config = resolveExceptionStepsConfig(config);
|
|
80
|
+
}
|
|
81
|
+
setConfig(config) {
|
|
82
|
+
this._config = resolveExceptionStepsConfig(config);
|
|
83
|
+
this._trimToMaxBytes();
|
|
84
|
+
}
|
|
85
|
+
add(step) {
|
|
86
|
+
const serialized = normalizeAndSerializeStep(step);
|
|
87
|
+
if (!serialized) return;
|
|
88
|
+
const bytes = getUtf8ByteLength(serialized.json);
|
|
89
|
+
if (bytes > this._config.max_bytes) return;
|
|
90
|
+
this._entries.push({
|
|
91
|
+
step: serialized.step,
|
|
92
|
+
bytes
|
|
93
|
+
});
|
|
94
|
+
this._totalBytes += bytes;
|
|
95
|
+
this._trimToMaxBytes();
|
|
96
|
+
}
|
|
97
|
+
getAttachable() {
|
|
98
|
+
return this._entries.map((e)=>e.step);
|
|
99
|
+
}
|
|
100
|
+
clear() {
|
|
101
|
+
this._entries = [];
|
|
102
|
+
this._totalBytes = 0;
|
|
103
|
+
}
|
|
104
|
+
size() {
|
|
105
|
+
return this._entries.length;
|
|
106
|
+
}
|
|
107
|
+
_trimToMaxBytes() {
|
|
108
|
+
while(this._totalBytes > this._config.max_bytes && this._entries.length > 0){
|
|
109
|
+
const evicted = this._entries.shift();
|
|
110
|
+
if (evicted) this._totalBytes -= evicted.bytes;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
function normalizePositiveInteger(input, fallback) {
|
|
115
|
+
if (!(0, index_js_namespaceObject.isNumber)(input) || input === 1 / 0 || input === -1 / 0) return fallback;
|
|
116
|
+
const normalized = Math.floor(input);
|
|
117
|
+
if (normalized < 0) return fallback;
|
|
118
|
+
return normalized;
|
|
119
|
+
}
|
|
120
|
+
function normalizeAndSerializeStep(step) {
|
|
121
|
+
const json = safeStringify(step);
|
|
122
|
+
if (!json) return;
|
|
123
|
+
try {
|
|
124
|
+
const parsed = JSON.parse(json);
|
|
125
|
+
if (!(0, index_js_namespaceObject.isObject)(parsed)) return;
|
|
126
|
+
const parsedStep = parsed;
|
|
127
|
+
const message = parsedStep[EXCEPTION_STEP_INTERNAL_FIELDS.MESSAGE];
|
|
128
|
+
const timestamp = parsedStep[EXCEPTION_STEP_INTERNAL_FIELDS.TIMESTAMP];
|
|
129
|
+
if (!(0, index_js_namespaceObject.isString)(message) || 0 === message.trim().length) return;
|
|
130
|
+
if (!(0, index_js_namespaceObject.isString)(timestamp) && !(0, index_js_namespaceObject.isNumber)(timestamp)) return;
|
|
131
|
+
return {
|
|
132
|
+
step: parsedStep,
|
|
133
|
+
json
|
|
134
|
+
};
|
|
135
|
+
} catch {
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
function safeStringify(value) {
|
|
140
|
+
const seen = new WeakSet();
|
|
141
|
+
try {
|
|
142
|
+
return JSON.stringify(value, (_key, replacementValue)=>{
|
|
143
|
+
if ('bigint' == typeof replacementValue) return replacementValue.toString();
|
|
144
|
+
if ('function' == typeof replacementValue || 'symbol' == typeof replacementValue) return;
|
|
145
|
+
if (replacementValue instanceof Date) return replacementValue.toISOString();
|
|
146
|
+
if (replacementValue instanceof Error) return {
|
|
147
|
+
name: replacementValue.name,
|
|
148
|
+
message: replacementValue.message,
|
|
149
|
+
stack: replacementValue.stack
|
|
150
|
+
};
|
|
151
|
+
if (replacementValue && 'object' == typeof replacementValue) {
|
|
152
|
+
if (seen.has(replacementValue)) return '[Circular]';
|
|
153
|
+
seen.add(replacementValue);
|
|
154
|
+
}
|
|
155
|
+
return replacementValue;
|
|
156
|
+
});
|
|
157
|
+
} catch {
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
function getUtf8ByteLength(value) {
|
|
162
|
+
if ('undefined' != typeof TextEncoder) return new TextEncoder().encode(value).length;
|
|
163
|
+
const encoded = encodeURIComponent(value);
|
|
164
|
+
let byteLength = 0;
|
|
165
|
+
for(let i = 0; i < encoded.length; i++)if ('%' === encoded[i]) {
|
|
166
|
+
byteLength += 1;
|
|
167
|
+
i += 2;
|
|
168
|
+
} else byteLength += 1;
|
|
169
|
+
return byteLength;
|
|
170
|
+
}
|
|
171
|
+
exports.DEFAULT_EXCEPTION_STEPS_CONFIG = __webpack_exports__.DEFAULT_EXCEPTION_STEPS_CONFIG;
|
|
172
|
+
exports.EXCEPTION_STEP_INTERNAL_FIELDS = __webpack_exports__.EXCEPTION_STEP_INTERNAL_FIELDS;
|
|
173
|
+
exports.ExceptionStepsBuffer = __webpack_exports__.ExceptionStepsBuffer;
|
|
174
|
+
exports.getUtf8ByteLength = __webpack_exports__.getUtf8ByteLength;
|
|
175
|
+
exports.resolveExceptionStepsConfig = __webpack_exports__.resolveExceptionStepsConfig;
|
|
176
|
+
exports.stripReservedExceptionStepFields = __webpack_exports__.stripReservedExceptionStepFields;
|
|
177
|
+
for(var __webpack_i__ in __webpack_exports__)if (-1 === [
|
|
178
|
+
"DEFAULT_EXCEPTION_STEPS_CONFIG",
|
|
179
|
+
"EXCEPTION_STEP_INTERNAL_FIELDS",
|
|
180
|
+
"ExceptionStepsBuffer",
|
|
181
|
+
"getUtf8ByteLength",
|
|
182
|
+
"resolveExceptionStepsConfig",
|
|
183
|
+
"stripReservedExceptionStepFields"
|
|
184
|
+
].indexOf(__webpack_i__)) exports[__webpack_i__] = __webpack_exports__[__webpack_i__];
|
|
185
|
+
Object.defineProperty(exports, '__esModule', {
|
|
186
|
+
value: true
|
|
187
|
+
});
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { isNumber, isObject, isString } from "../utils/index.mjs";
|
|
2
|
+
const EXCEPTION_STEP_INTERNAL_FIELDS = {
|
|
3
|
+
MESSAGE: '$message',
|
|
4
|
+
TIMESTAMP: '$timestamp'
|
|
5
|
+
};
|
|
6
|
+
const RESERVED_EXCEPTION_STEP_KEYS = new Set([
|
|
7
|
+
EXCEPTION_STEP_INTERNAL_FIELDS.MESSAGE,
|
|
8
|
+
EXCEPTION_STEP_INTERNAL_FIELDS.TIMESTAMP
|
|
9
|
+
]);
|
|
10
|
+
const DEFAULT_EXCEPTION_STEPS_CONFIG = {
|
|
11
|
+
enabled: true,
|
|
12
|
+
max_bytes: 32768
|
|
13
|
+
};
|
|
14
|
+
function resolveExceptionStepsConfig(config) {
|
|
15
|
+
if (!config) return {
|
|
16
|
+
...DEFAULT_EXCEPTION_STEPS_CONFIG
|
|
17
|
+
};
|
|
18
|
+
return {
|
|
19
|
+
enabled: config.enabled ?? DEFAULT_EXCEPTION_STEPS_CONFIG.enabled,
|
|
20
|
+
max_bytes: normalizePositiveInteger(config.max_bytes, DEFAULT_EXCEPTION_STEPS_CONFIG.max_bytes)
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
function stripReservedExceptionStepFields(properties) {
|
|
24
|
+
if (!properties) return {
|
|
25
|
+
sanitizedProperties: {},
|
|
26
|
+
droppedKeys: []
|
|
27
|
+
};
|
|
28
|
+
const droppedKeys = [];
|
|
29
|
+
const sanitizedProperties = Object.keys(properties).reduce((acc, key)=>{
|
|
30
|
+
if (RESERVED_EXCEPTION_STEP_KEYS.has(key)) {
|
|
31
|
+
droppedKeys.push(key);
|
|
32
|
+
return acc;
|
|
33
|
+
}
|
|
34
|
+
acc[key] = properties[key];
|
|
35
|
+
return acc;
|
|
36
|
+
}, {});
|
|
37
|
+
return {
|
|
38
|
+
sanitizedProperties,
|
|
39
|
+
droppedKeys
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
class ExceptionStepsBuffer {
|
|
43
|
+
constructor(config){
|
|
44
|
+
this._entries = [];
|
|
45
|
+
this._totalBytes = 0;
|
|
46
|
+
this._config = resolveExceptionStepsConfig(config);
|
|
47
|
+
}
|
|
48
|
+
setConfig(config) {
|
|
49
|
+
this._config = resolveExceptionStepsConfig(config);
|
|
50
|
+
this._trimToMaxBytes();
|
|
51
|
+
}
|
|
52
|
+
add(step) {
|
|
53
|
+
const serialized = normalizeAndSerializeStep(step);
|
|
54
|
+
if (!serialized) return;
|
|
55
|
+
const bytes = getUtf8ByteLength(serialized.json);
|
|
56
|
+
if (bytes > this._config.max_bytes) return;
|
|
57
|
+
this._entries.push({
|
|
58
|
+
step: serialized.step,
|
|
59
|
+
bytes
|
|
60
|
+
});
|
|
61
|
+
this._totalBytes += bytes;
|
|
62
|
+
this._trimToMaxBytes();
|
|
63
|
+
}
|
|
64
|
+
getAttachable() {
|
|
65
|
+
return this._entries.map((e)=>e.step);
|
|
66
|
+
}
|
|
67
|
+
clear() {
|
|
68
|
+
this._entries = [];
|
|
69
|
+
this._totalBytes = 0;
|
|
70
|
+
}
|
|
71
|
+
size() {
|
|
72
|
+
return this._entries.length;
|
|
73
|
+
}
|
|
74
|
+
_trimToMaxBytes() {
|
|
75
|
+
while(this._totalBytes > this._config.max_bytes && this._entries.length > 0){
|
|
76
|
+
const evicted = this._entries.shift();
|
|
77
|
+
if (evicted) this._totalBytes -= evicted.bytes;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
function normalizePositiveInteger(input, fallback) {
|
|
82
|
+
if (!isNumber(input) || input === 1 / 0 || input === -1 / 0) return fallback;
|
|
83
|
+
const normalized = Math.floor(input);
|
|
84
|
+
if (normalized < 0) return fallback;
|
|
85
|
+
return normalized;
|
|
86
|
+
}
|
|
87
|
+
function normalizeAndSerializeStep(step) {
|
|
88
|
+
const json = safeStringify(step);
|
|
89
|
+
if (!json) return;
|
|
90
|
+
try {
|
|
91
|
+
const parsed = JSON.parse(json);
|
|
92
|
+
if (!isObject(parsed)) return;
|
|
93
|
+
const parsedStep = parsed;
|
|
94
|
+
const message = parsedStep[EXCEPTION_STEP_INTERNAL_FIELDS.MESSAGE];
|
|
95
|
+
const timestamp = parsedStep[EXCEPTION_STEP_INTERNAL_FIELDS.TIMESTAMP];
|
|
96
|
+
if (!isString(message) || 0 === message.trim().length) return;
|
|
97
|
+
if (!isString(timestamp) && !isNumber(timestamp)) return;
|
|
98
|
+
return {
|
|
99
|
+
step: parsedStep,
|
|
100
|
+
json
|
|
101
|
+
};
|
|
102
|
+
} catch {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
function safeStringify(value) {
|
|
107
|
+
const seen = new WeakSet();
|
|
108
|
+
try {
|
|
109
|
+
return JSON.stringify(value, (_key, replacementValue)=>{
|
|
110
|
+
if ('bigint' == typeof replacementValue) return replacementValue.toString();
|
|
111
|
+
if ('function' == typeof replacementValue || 'symbol' == typeof replacementValue) return;
|
|
112
|
+
if (replacementValue instanceof Date) return replacementValue.toISOString();
|
|
113
|
+
if (replacementValue instanceof Error) return {
|
|
114
|
+
name: replacementValue.name,
|
|
115
|
+
message: replacementValue.message,
|
|
116
|
+
stack: replacementValue.stack
|
|
117
|
+
};
|
|
118
|
+
if (replacementValue && 'object' == typeof replacementValue) {
|
|
119
|
+
if (seen.has(replacementValue)) return '[Circular]';
|
|
120
|
+
seen.add(replacementValue);
|
|
121
|
+
}
|
|
122
|
+
return replacementValue;
|
|
123
|
+
});
|
|
124
|
+
} catch {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
function getUtf8ByteLength(value) {
|
|
129
|
+
if ('undefined' != typeof TextEncoder) return new TextEncoder().encode(value).length;
|
|
130
|
+
const encoded = encodeURIComponent(value);
|
|
131
|
+
let byteLength = 0;
|
|
132
|
+
for(let i = 0; i < encoded.length; i++)if ('%' === encoded[i]) {
|
|
133
|
+
byteLength += 1;
|
|
134
|
+
i += 2;
|
|
135
|
+
} else byteLength += 1;
|
|
136
|
+
return byteLength;
|
|
137
|
+
}
|
|
138
|
+
export { DEFAULT_EXCEPTION_STEPS_CONFIG, EXCEPTION_STEP_INTERNAL_FIELDS, ExceptionStepsBuffer, getUtf8ByteLength, resolveExceptionStepsConfig, stripReservedExceptionStepFields };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/error-tracking/index.ts"],"names":[],"mappings":"AAAA,cAAc,4BAA4B,CAAA;AAC1C,mBAAmB,SAAS,CAAA;AAC5B,cAAc,WAAW,CAAA;AACzB,cAAc,YAAY,CAAA;AAC1B,cAAc,SAAS,CAAA"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/error-tracking/index.ts"],"names":[],"mappings":"AAAA,cAAc,4BAA4B,CAAA;AAC1C,mBAAmB,SAAS,CAAA;AAC5B,cAAc,WAAW,CAAA;AACzB,cAAc,YAAY,CAAA;AAC1B,cAAc,SAAS,CAAA;AACvB,cAAc,mBAAmB,CAAA"}
|
|
@@ -6,6 +6,9 @@ var __webpack_modules__ = {
|
|
|
6
6
|
"./error-properties-builder": function(module) {
|
|
7
7
|
module.exports = require("./error-properties-builder.js");
|
|
8
8
|
},
|
|
9
|
+
"./exception-steps": function(module) {
|
|
10
|
+
module.exports = require("./exception-steps.js");
|
|
11
|
+
},
|
|
9
12
|
"./parsers": function(module) {
|
|
10
13
|
module.exports = require("./parsers/index.js");
|
|
11
14
|
},
|
|
@@ -80,6 +83,12 @@ var __webpack_exports__ = {};
|
|
|
80
83
|
return _utils__WEBPACK_IMPORTED_MODULE_3__[key];
|
|
81
84
|
}).bind(0, __WEBPACK_IMPORT_KEY__);
|
|
82
85
|
__webpack_require__.d(__webpack_exports__, __WEBPACK_REEXPORT_OBJECT__);
|
|
86
|
+
var _exception_steps__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__("./exception-steps");
|
|
87
|
+
var __WEBPACK_REEXPORT_OBJECT__ = {};
|
|
88
|
+
for(var __WEBPACK_IMPORT_KEY__ in _exception_steps__WEBPACK_IMPORTED_MODULE_4__)if ("default" !== __WEBPACK_IMPORT_KEY__) __WEBPACK_REEXPORT_OBJECT__[__WEBPACK_IMPORT_KEY__] = (function(key) {
|
|
89
|
+
return _exception_steps__WEBPACK_IMPORTED_MODULE_4__[key];
|
|
90
|
+
}).bind(0, __WEBPACK_IMPORT_KEY__);
|
|
91
|
+
__webpack_require__.d(__webpack_exports__, __WEBPACK_REEXPORT_OBJECT__);
|
|
83
92
|
})();
|
|
84
93
|
for(var __webpack_i__ in __webpack_exports__)exports[__webpack_i__] = __webpack_exports__[__webpack_i__];
|
|
85
94
|
Object.defineProperty(exports, '__esModule', {
|
package/package.json
CHANGED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import {
|
|
2
|
+
EXCEPTION_STEP_INTERNAL_FIELDS,
|
|
3
|
+
ExceptionStep,
|
|
4
|
+
ExceptionStepsBuffer,
|
|
5
|
+
getUtf8ByteLength,
|
|
6
|
+
resolveExceptionStepsConfig,
|
|
7
|
+
stripReservedExceptionStepFields,
|
|
8
|
+
} from './exception-steps'
|
|
9
|
+
|
|
10
|
+
describe('exception steps', () => {
|
|
11
|
+
describe('resolveExceptionStepsConfig', () => {
|
|
12
|
+
it('uses defaults when no config is passed', () => {
|
|
13
|
+
expect(resolveExceptionStepsConfig()).toEqual({
|
|
14
|
+
enabled: true,
|
|
15
|
+
max_bytes: 32768,
|
|
16
|
+
})
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it('falls back to defaults for invalid values', () => {
|
|
20
|
+
expect(resolveExceptionStepsConfig({ max_bytes: Number.NaN })).toEqual({
|
|
21
|
+
enabled: true,
|
|
22
|
+
max_bytes: 32768,
|
|
23
|
+
})
|
|
24
|
+
})
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
describe('stripReservedExceptionStepFields', () => {
|
|
28
|
+
it('strips reserved fields and keeps custom properties', () => {
|
|
29
|
+
const result = stripReservedExceptionStepFields({
|
|
30
|
+
[EXCEPTION_STEP_INTERNAL_FIELDS.MESSAGE]: 'should-strip',
|
|
31
|
+
[EXCEPTION_STEP_INTERNAL_FIELDS.TIMESTAMP]: 'should-strip',
|
|
32
|
+
custom: true,
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
expect(result).toEqual({
|
|
36
|
+
sanitizedProperties: { custom: true },
|
|
37
|
+
droppedKeys: [EXCEPTION_STEP_INTERNAL_FIELDS.MESSAGE, EXCEPTION_STEP_INTERNAL_FIELDS.TIMESTAMP],
|
|
38
|
+
})
|
|
39
|
+
})
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
describe('ExceptionStepsBuffer', () => {
|
|
43
|
+
const makeStep = (message: string): ExceptionStep => ({
|
|
44
|
+
[EXCEPTION_STEP_INTERNAL_FIELDS.MESSAGE]: message,
|
|
45
|
+
[EXCEPTION_STEP_INTERNAL_FIELDS.TIMESTAMP]: '2026-01-01T00:00:00.000Z',
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
const bytesOf = (step: ExceptionStep) => getUtf8ByteLength(JSON.stringify(step))
|
|
49
|
+
|
|
50
|
+
it.each([
|
|
51
|
+
{
|
|
52
|
+
desc: 'evicts oldest steps when max bytes are exceeded',
|
|
53
|
+
config: { max_bytes: bytesOf(makeStep('two')) + bytesOf(makeStep('three')) },
|
|
54
|
+
steps: [makeStep('one'), makeStep('two'), makeStep('three')],
|
|
55
|
+
expected: ['two', 'three'],
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
desc: 'keeps the most recent step when budget fits only one',
|
|
59
|
+
config: { max_bytes: bytesOf(makeStep('one')) },
|
|
60
|
+
steps: [makeStep('one'), makeStep('two')],
|
|
61
|
+
expected: ['two'],
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
desc: 'skips malformed steps that cannot be normalized',
|
|
65
|
+
config: { max_bytes: 10000 },
|
|
66
|
+
steps: [{ $message: '', $timestamp: '2026-01-01T00:00:00.000Z' } as ExceptionStep, makeStep('valid')],
|
|
67
|
+
expected: ['valid'],
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
desc: 'drops a step that exceeds the entire budget on its own',
|
|
71
|
+
config: { max_bytes: 10 },
|
|
72
|
+
steps: [makeStep('this message is way too long for the tiny budget')],
|
|
73
|
+
expected: [],
|
|
74
|
+
},
|
|
75
|
+
])('$desc', ({ config, steps, expected }) => {
|
|
76
|
+
const buffer = new ExceptionStepsBuffer(config)
|
|
77
|
+
for (const step of steps) {
|
|
78
|
+
buffer.add(step)
|
|
79
|
+
}
|
|
80
|
+
expect(buffer.getAttachable().map((s) => s.$message)).toEqual(expected)
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
it('clears all steps', () => {
|
|
84
|
+
const buffer = new ExceptionStepsBuffer({ max_bytes: 10000 })
|
|
85
|
+
buffer.add(makeStep('one'))
|
|
86
|
+
buffer.clear()
|
|
87
|
+
expect(buffer.size()).toBe(0)
|
|
88
|
+
})
|
|
89
|
+
})
|
|
90
|
+
})
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import { isArray, isNumber, isObject, isString } from '@/utils'
|
|
2
|
+
|
|
3
|
+
export const EXCEPTION_STEP_INTERNAL_FIELDS = {
|
|
4
|
+
MESSAGE: '$message',
|
|
5
|
+
TIMESTAMP: '$timestamp',
|
|
6
|
+
} as const
|
|
7
|
+
|
|
8
|
+
const RESERVED_EXCEPTION_STEP_KEYS = new Set<string>([
|
|
9
|
+
EXCEPTION_STEP_INTERNAL_FIELDS.MESSAGE,
|
|
10
|
+
EXCEPTION_STEP_INTERNAL_FIELDS.TIMESTAMP,
|
|
11
|
+
])
|
|
12
|
+
|
|
13
|
+
export type ExceptionStep = {
|
|
14
|
+
[EXCEPTION_STEP_INTERNAL_FIELDS.MESSAGE]: string
|
|
15
|
+
[EXCEPTION_STEP_INTERNAL_FIELDS.TIMESTAMP]: string | number
|
|
16
|
+
[key: string]: unknown
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** NOTE: This type is also defined in `@posthog/types` (posthog-config.ts). Keep both in sync. */
|
|
20
|
+
export type ExceptionStepsConfig = {
|
|
21
|
+
enabled?: boolean
|
|
22
|
+
max_bytes?: number
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export type ResolvedExceptionStepsConfig = {
|
|
26
|
+
enabled: boolean
|
|
27
|
+
max_bytes: number
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const DEFAULT_EXCEPTION_STEPS_CONFIG: ResolvedExceptionStepsConfig = {
|
|
31
|
+
enabled: true,
|
|
32
|
+
max_bytes: 32768, // ~32KB
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function resolveExceptionStepsConfig(config?: ExceptionStepsConfig | null): ResolvedExceptionStepsConfig {
|
|
36
|
+
if (!config) {
|
|
37
|
+
return { ...DEFAULT_EXCEPTION_STEPS_CONFIG }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
enabled: config.enabled ?? DEFAULT_EXCEPTION_STEPS_CONFIG.enabled,
|
|
42
|
+
max_bytes: normalizePositiveInteger(config.max_bytes, DEFAULT_EXCEPTION_STEPS_CONFIG.max_bytes),
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function stripReservedExceptionStepFields(properties?: Record<string, unknown> | null): {
|
|
47
|
+
sanitizedProperties: Record<string, unknown>
|
|
48
|
+
droppedKeys: string[]
|
|
49
|
+
} {
|
|
50
|
+
if (!properties) {
|
|
51
|
+
return { sanitizedProperties: {}, droppedKeys: [] }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const droppedKeys: string[] = []
|
|
55
|
+
const sanitizedProperties = Object.keys(properties).reduce<Record<string, unknown>>((acc, key) => {
|
|
56
|
+
if (RESERVED_EXCEPTION_STEP_KEYS.has(key)) {
|
|
57
|
+
droppedKeys.push(key)
|
|
58
|
+
return acc
|
|
59
|
+
}
|
|
60
|
+
acc[key] = properties[key]
|
|
61
|
+
return acc
|
|
62
|
+
}, {})
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
sanitizedProperties,
|
|
66
|
+
droppedKeys,
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export class ExceptionStepsBuffer {
|
|
71
|
+
private _entries: { step: ExceptionStep; bytes: number }[] = []
|
|
72
|
+
private _totalBytes: number = 0
|
|
73
|
+
private _config: ResolvedExceptionStepsConfig
|
|
74
|
+
|
|
75
|
+
constructor(config?: ExceptionStepsConfig | null) {
|
|
76
|
+
this._config = resolveExceptionStepsConfig(config)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
public setConfig(config?: ExceptionStepsConfig | null): void {
|
|
80
|
+
this._config = resolveExceptionStepsConfig(config)
|
|
81
|
+
this._trimToMaxBytes()
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
public add(step: ExceptionStep): void {
|
|
85
|
+
const serialized = normalizeAndSerializeStep(step)
|
|
86
|
+
if (!serialized) {
|
|
87
|
+
return
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const bytes = getUtf8ByteLength(serialized.json)
|
|
91
|
+
if (bytes > this._config.max_bytes) {
|
|
92
|
+
return
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
this._entries.push({ step: serialized.step, bytes })
|
|
96
|
+
this._totalBytes += bytes
|
|
97
|
+
this._trimToMaxBytes()
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
public getAttachable(): ExceptionStep[] {
|
|
101
|
+
return this._entries.map((e) => e.step)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
public clear(): void {
|
|
105
|
+
this._entries = []
|
|
106
|
+
this._totalBytes = 0
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
public size(): number {
|
|
110
|
+
return this._entries.length
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
private _trimToMaxBytes(): void {
|
|
114
|
+
while (this._totalBytes > this._config.max_bytes && this._entries.length > 0) {
|
|
115
|
+
const evicted = this._entries.shift()
|
|
116
|
+
if (evicted) {
|
|
117
|
+
this._totalBytes -= evicted.bytes
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function normalizePositiveInteger(input: number | undefined, fallback: number): number {
|
|
124
|
+
if (!isNumber(input) || input === Infinity || input === -Infinity) {
|
|
125
|
+
return fallback
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const normalized = Math.floor(input)
|
|
129
|
+
if (normalized < 0) {
|
|
130
|
+
return fallback
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return normalized
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function normalizeAndSerializeStep(step: ExceptionStep): { step: ExceptionStep; json: string } | undefined {
|
|
137
|
+
const json = safeStringify(step)
|
|
138
|
+
if (!json) {
|
|
139
|
+
return undefined
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
try {
|
|
143
|
+
const parsed = JSON.parse(json)
|
|
144
|
+
if (!isObject(parsed)) {
|
|
145
|
+
return undefined
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const parsedStep = parsed as Record<string, unknown>
|
|
149
|
+
const message = parsedStep[EXCEPTION_STEP_INTERNAL_FIELDS.MESSAGE]
|
|
150
|
+
const timestamp = parsedStep[EXCEPTION_STEP_INTERNAL_FIELDS.TIMESTAMP]
|
|
151
|
+
|
|
152
|
+
if (!isString(message) || message.trim().length === 0) {
|
|
153
|
+
return undefined
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (!isString(timestamp) && !isNumber(timestamp)) {
|
|
157
|
+
return undefined
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return {
|
|
161
|
+
step: parsedStep as ExceptionStep,
|
|
162
|
+
json,
|
|
163
|
+
}
|
|
164
|
+
} catch {
|
|
165
|
+
return undefined
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function safeStringify(value: unknown): string | undefined {
|
|
170
|
+
const seen = new WeakSet<object>()
|
|
171
|
+
|
|
172
|
+
try {
|
|
173
|
+
return JSON.stringify(value, (_key, replacementValue: unknown) => {
|
|
174
|
+
if (typeof replacementValue === 'bigint') {
|
|
175
|
+
return replacementValue.toString()
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (typeof replacementValue === 'function' || typeof replacementValue === 'symbol') {
|
|
179
|
+
return undefined
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (replacementValue instanceof Date) {
|
|
183
|
+
return replacementValue.toISOString()
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (replacementValue instanceof Error) {
|
|
187
|
+
return {
|
|
188
|
+
name: replacementValue.name,
|
|
189
|
+
message: replacementValue.message,
|
|
190
|
+
stack: replacementValue.stack,
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (replacementValue && typeof replacementValue === 'object') {
|
|
195
|
+
if (seen.has(replacementValue)) {
|
|
196
|
+
return '[Circular]'
|
|
197
|
+
}
|
|
198
|
+
seen.add(replacementValue)
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return replacementValue
|
|
202
|
+
})
|
|
203
|
+
} catch {
|
|
204
|
+
return undefined
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export function getUtf8ByteLength(value: string): number {
|
|
209
|
+
if (typeof TextEncoder !== 'undefined') {
|
|
210
|
+
return new TextEncoder().encode(value).length
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const encoded = encodeURIComponent(value)
|
|
214
|
+
let byteLength = 0
|
|
215
|
+
for (let i = 0; i < encoded.length; i++) {
|
|
216
|
+
if (encoded[i] === '%') {
|
|
217
|
+
byteLength += 1
|
|
218
|
+
i += 2
|
|
219
|
+
} else {
|
|
220
|
+
byteLength += 1
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return byteLength
|
|
225
|
+
}
|