expensify-common 2.0.190 → 2.0.192

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/Logger.d.ts CHANGED
@@ -21,6 +21,26 @@ type LoggerOptions = {
21
21
  maxLogLinesBeforeFlush?: number;
22
22
  getContextEmail?: () => string | null;
23
23
  };
24
+ declare const MAX_LOG_LINE_BYTES = 1000000;
25
+ /**
26
+ * Gets the total UTF-8 byte length of a string.
27
+ */
28
+ declare function utf8ByteLength(input: string): number;
29
+ /**
30
+ * Truncates `message` so that the SERIALIZED `line` fits within maxSize bytes. The serialized
31
+ * line is `overhead(empty message) + JSON-escaped bytes of the message`, so we measure the
32
+ * message's escaped size directly (single pass) instead of repeatedly re-serializing the whole
33
+ * line. This is exact for JSON.stringify's escaping and avoids the cost of a binary search.
34
+ */
35
+ declare function truncateMessageToFitLine(line: LogLine, message: string, maxSize: number): string;
36
+ /**
37
+ * Serializes a log line while enforcing the per-line byte limit on the *serialized* line — what
38
+ * the server measures — covering the message, parameters and metadata plus JSON-escaping
39
+ * overhead. Returns the JSON string for the line (reused to build the packet, so each line is
40
+ * serialized only once). Oversized `parameters` (structured data we can't safely truncate
41
+ * mid-JSON) are replaced with a size marker; the message is then truncated to fit the remainder.
42
+ */
43
+ declare function serializeLineWithinByteLimit(line: LogLine, maxSize: number): string;
24
44
  export default class Logger {
25
45
  logLines: LogLine[];
26
46
  serverLoggingCallback: ServerLoggingCallback;
@@ -79,4 +99,4 @@ export default class Logger {
79
99
  */
80
100
  client(message: string, extraData?: Parameters): void;
81
101
  }
82
- export {};
102
+ export { truncateMessageToFitLine, serializeLineWithinByteLimit, utf8ByteLength, MAX_LOG_LINE_BYTES };
package/dist/Logger.js CHANGED
@@ -1,11 +1,115 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MAX_LOG_LINE_BYTES = void 0;
4
+ exports.truncateMessageToFitLine = truncateMessageToFitLine;
5
+ exports.serializeLineWithinByteLimit = serializeLineWithinByteLimit;
6
+ exports.utf8ByteLength = utf8ByteLength;
3
7
  const MAX_LOG_LINES_BEFORE_FLUSH = 50;
4
- // The server drops (and alerts on) any single log message whose byte length exceeds 1MB, so cap
5
- // each message just below that. We truncate rather than drop so oversized lines are still logged
6
- // (head of the message + a marker) instead of being lost. The margin leaves room for the marker.
7
- const ONE_MEGABYTE = 1024 * 1024;
8
- const MAX_LOG_MESSAGE_LENGTH = ONE_MEGABYTE - 1024;
8
+ // The server rejects any single serialized log line larger than 1,048,576 bytes (1 MiB).
9
+ // We enforce a lower cap on the JSON-serialized line (message + parameters + metadata, with
10
+ // escaping) so it stays comfortably under the server limit.
11
+ const MAX_LOG_LINE_BYTES = 1000000;
12
+ exports.MAX_LOG_LINE_BYTES = MAX_LOG_LINE_BYTES;
13
+ /**
14
+ * Gets the UTF-8 byte length of a single unicode code point.
15
+ */
16
+ function codePointByteSize(code) {
17
+ if (code >= 0x10000) {
18
+ return 4;
19
+ }
20
+ if (code >= 0x800) {
21
+ return 3;
22
+ }
23
+ if (code >= 0x80) {
24
+ return 2;
25
+ }
26
+ return 1;
27
+ }
28
+ /**
29
+ * Gets the total UTF-8 byte length of a string.
30
+ */
31
+ function utf8ByteLength(input) {
32
+ return Array.from(input).reduce((sum, char) => { var _a; return sum + codePointByteSize((_a = char.codePointAt(0)) !== null && _a !== void 0 ? _a : 0); }, 0);
33
+ }
34
+ /**
35
+ * UTF-8 byte length of a single code point *after JSON string escaping*, matching the output of
36
+ * JSON.stringify: `"` `\` and the short control escapes are 2 bytes, other control characters
37
+ * and lone surrogates become `\uXXXX` (6 bytes), everything else is its plain UTF-8 size.
38
+ */
39
+ function jsonEscapedByteSize(code) {
40
+ if (code === 0x22 || code === 0x5c || code === 0x08 || code === 0x09 || code === 0x0a || code === 0x0c || code === 0x0d) {
41
+ return 2;
42
+ }
43
+ if (code < 0x20 || (code >= 0xd800 && code <= 0xdfff)) {
44
+ return 6;
45
+ }
46
+ return codePointByteSize(code);
47
+ }
48
+ /**
49
+ * Truncates `message` so that the SERIALIZED `line` fits within maxSize bytes. The serialized
50
+ * line is `overhead(empty message) + JSON-escaped bytes of the message`, so we measure the
51
+ * message's escaped size directly (single pass) instead of repeatedly re-serializing the whole
52
+ * line. This is exact for JSON.stringify's escaping and avoids the cost of a binary search.
53
+ */
54
+ function truncateMessageToFitLine(line, message, maxSize) {
55
+ var _a;
56
+ const overhead = utf8ByteLength(JSON.stringify(Object.assign(Object.assign({}, line), { message: '' })));
57
+ const totalRawBytes = utf8ByteLength(message);
58
+ // Marker "...[truncated N bytes]" is escape-free ASCII, so its serialized size equals its
59
+ // raw size = 21 + digits(N). Reserve for the max possible N so the final line never overflows.
60
+ const MARKER_STATIC_BYTES = 21;
61
+ const reservedMarkerBytes = MARKER_STATIC_BYTES + String(totalRawBytes).length;
62
+ const contentBudget = maxSize - overhead - reservedMarkerBytes;
63
+ if (contentBudget <= 0) {
64
+ return '';
65
+ }
66
+ // Keep whole code points until the escaped budget is exhausted (never splits a character).
67
+ let keptUnits = 0;
68
+ let keptEscapedBytes = 0;
69
+ let keptRawBytes = 0;
70
+ for (let i = 0; i < message.length;) {
71
+ const code = (_a = message.codePointAt(i)) !== null && _a !== void 0 ? _a : 0;
72
+ const escapedBytes = jsonEscapedByteSize(code);
73
+ if (keptEscapedBytes + escapedBytes > contentBudget) {
74
+ break;
75
+ }
76
+ keptEscapedBytes += escapedBytes;
77
+ keptRawBytes += codePointByteSize(code);
78
+ const units = code > 0xffff ? 2 : 1;
79
+ i += units;
80
+ keptUnits += units;
81
+ }
82
+ if (keptRawBytes >= totalRawBytes) {
83
+ return message;
84
+ }
85
+ const removed = totalRawBytes - keptRawBytes;
86
+ return `${message.slice(0, keptUnits)}...[truncated ${removed} bytes]`;
87
+ }
88
+ /**
89
+ * Serializes a log line while enforcing the per-line byte limit on the *serialized* line — what
90
+ * the server measures — covering the message, parameters and metadata plus JSON-escaping
91
+ * overhead. Returns the JSON string for the line (reused to build the packet, so each line is
92
+ * serialized only once). Oversized `parameters` (structured data we can't safely truncate
93
+ * mid-JSON) are replaced with a size marker; the message is then truncated to fit the remainder.
94
+ */
95
+ function serializeLineWithinByteLimit(line, maxSize) {
96
+ var _a;
97
+ const serialized = JSON.stringify(line);
98
+ // Cheap fast path: at most 3 UTF-8 bytes per UTF-16 code unit, so if 3 * length fits the
99
+ // line is definitely under the limit and we avoid the exact byte count entirely.
100
+ if (serialized.length * 3 <= maxSize || utf8ByteLength(serialized) <= maxSize) {
101
+ return serialized;
102
+ }
103
+ const result = Object.assign({}, line);
104
+ // If the line is over the limit even with an empty message, the bulk is in `parameters` —
105
+ // replace it with a marker so the (human-readable) message is what we keep room for.
106
+ if (utf8ByteLength(JSON.stringify(Object.assign(Object.assign({}, result), { message: '' }))) > maxSize) {
107
+ const parametersByteSize = utf8ByteLength(JSON.stringify((_a = result.parameters) !== null && _a !== void 0 ? _a : ''));
108
+ result.parameters = { truncated: true, originalByteSize: parametersByteSize };
109
+ }
110
+ result.message = truncateMessageToFitLine(result, line.message, maxSize);
111
+ return JSON.stringify(result);
112
+ }
9
113
  class Logger {
10
114
  constructor({ serverLoggingCallback, isDebug, clientLoggingCallback, maxLogLinesBeforeFlush, getContextEmail }) {
11
115
  // An array of log lines that limits itself to a certain number of entries (deleting the oldest)
@@ -31,16 +135,20 @@ class Logger {
31
135
  if (!this.logLines.length || ((_a = this.logLines) === null || _a === void 0 ? void 0 : _a.every((l) => l.onlyFlushWithOthers))) {
32
136
  return;
33
137
  }
34
- // We don't care about log setting web cookies so let's define it as false
35
- const linesToLog = (_b = this.logLines) === null || _b === void 0 ? void 0 : _b.map((l) => {
138
+ // We don't care about log setting web cookies so let's define it as false.
139
+ // Serialize each line while bounding it to the server's per-line size limit (covers
140
+ // message, parameters and JSON-escaping overhead). Building the packet by joining the
141
+ // per-line JSON keeps each line serialized only once — identical output to
142
+ // JSON.stringify(array) with no extra pass.
143
+ const serializedLines = (_b = this.logLines) === null || _b === void 0 ? void 0 : _b.map((l) => {
36
144
  // eslint-disable-next-line no-param-reassign
37
145
  delete l.onlyFlushWithOthers;
38
- return l;
146
+ return serializeLineWithinByteLimit(l, MAX_LOG_LINE_BYTES);
39
147
  });
40
148
  this.logLines = [];
41
149
  const promise = this.serverLoggingCallback(this, {
42
150
  api_setCookie: false,
43
- logPacket: JSON.stringify(linesToLog),
151
+ logPacket: `[${serializedLines.join(',')}]`,
44
152
  });
45
153
  if (!promise) {
46
154
  return;
@@ -68,13 +176,8 @@ class Logger {
68
176
  catch (_a) {
69
177
  // Silently fail if getContextEmail throws - logging should not crash
70
178
  }
71
- let cappedMessage = message;
72
- if (message.length > MAX_LOG_MESSAGE_LENGTH) {
73
- const omittedCount = message.length - MAX_LOG_MESSAGE_LENGTH;
74
- cappedMessage = `${message.slice(0, MAX_LOG_MESSAGE_LENGTH)}…[truncated ${omittedCount} characters]`;
75
- }
76
179
  const length = this.logLines.push({
77
- message: cappedMessage,
180
+ message,
78
181
  parameters,
79
182
  onlyFlushWithOthers,
80
183
  timestamp: new Date(),
@@ -21,6 +21,26 @@ type LoggerOptions = {
21
21
  maxLogLinesBeforeFlush?: number;
22
22
  getContextEmail?: () => string | null;
23
23
  };
24
+ declare const MAX_LOG_LINE_BYTES = 1000000;
25
+ /**
26
+ * Gets the total UTF-8 byte length of a string.
27
+ */
28
+ declare function utf8ByteLength(input: string): number;
29
+ /**
30
+ * Truncates `message` so that the SERIALIZED `line` fits within maxSize bytes. The serialized
31
+ * line is `overhead(empty message) + JSON-escaped bytes of the message`, so we measure the
32
+ * message's escaped size directly (single pass) instead of repeatedly re-serializing the whole
33
+ * line. This is exact for JSON.stringify's escaping and avoids the cost of a binary search.
34
+ */
35
+ declare function truncateMessageToFitLine(line: LogLine, message: string, maxSize: number): string;
36
+ /**
37
+ * Serializes a log line while enforcing the per-line byte limit on the *serialized* line — what
38
+ * the server measures — covering the message, parameters and metadata plus JSON-escaping
39
+ * overhead. Returns the JSON string for the line (reused to build the packet, so each line is
40
+ * serialized only once). Oversized `parameters` (structured data we can't safely truncate
41
+ * mid-JSON) are replaced with a size marker; the message is then truncated to fit the remainder.
42
+ */
43
+ declare function serializeLineWithinByteLimit(line: LogLine, maxSize: number): string;
24
44
  export default class Logger {
25
45
  logLines: LogLine[];
26
46
  serverLoggingCallback: ServerLoggingCallback;
@@ -79,4 +99,4 @@ export default class Logger {
79
99
  */
80
100
  client(message: string, extraData?: Parameters): void;
81
101
  }
82
- export {};
102
+ export { truncateMessageToFitLine, serializeLineWithinByteLimit, utf8ByteLength, MAX_LOG_LINE_BYTES };
@@ -1,9 +1,108 @@
1
1
  const MAX_LOG_LINES_BEFORE_FLUSH = 50;
2
- // The server drops (and alerts on) any single log message whose byte length exceeds 1MB, so cap
3
- // each message just below that. We truncate rather than drop so oversized lines are still logged
4
- // (head of the message + a marker) instead of being lost. The margin leaves room for the marker.
5
- const ONE_MEGABYTE = 1024 * 1024;
6
- const MAX_LOG_MESSAGE_LENGTH = ONE_MEGABYTE - 1024;
2
+ // The server rejects any single serialized log line larger than 1,048,576 bytes (1 MiB).
3
+ // We enforce a lower cap on the JSON-serialized line (message + parameters + metadata, with
4
+ // escaping) so it stays comfortably under the server limit.
5
+ const MAX_LOG_LINE_BYTES = 1000000;
6
+ /**
7
+ * Gets the UTF-8 byte length of a single unicode code point.
8
+ */
9
+ function codePointByteSize(code) {
10
+ if (code >= 0x10000) {
11
+ return 4;
12
+ }
13
+ if (code >= 0x800) {
14
+ return 3;
15
+ }
16
+ if (code >= 0x80) {
17
+ return 2;
18
+ }
19
+ return 1;
20
+ }
21
+ /**
22
+ * Gets the total UTF-8 byte length of a string.
23
+ */
24
+ function utf8ByteLength(input) {
25
+ return Array.from(input).reduce((sum, char) => { var _a; return sum + codePointByteSize((_a = char.codePointAt(0)) !== null && _a !== void 0 ? _a : 0); }, 0);
26
+ }
27
+ /**
28
+ * UTF-8 byte length of a single code point *after JSON string escaping*, matching the output of
29
+ * JSON.stringify: `"` `\` and the short control escapes are 2 bytes, other control characters
30
+ * and lone surrogates become `\uXXXX` (6 bytes), everything else is its plain UTF-8 size.
31
+ */
32
+ function jsonEscapedByteSize(code) {
33
+ if (code === 0x22 || code === 0x5c || code === 0x08 || code === 0x09 || code === 0x0a || code === 0x0c || code === 0x0d) {
34
+ return 2;
35
+ }
36
+ if (code < 0x20 || (code >= 0xd800 && code <= 0xdfff)) {
37
+ return 6;
38
+ }
39
+ return codePointByteSize(code);
40
+ }
41
+ /**
42
+ * Truncates `message` so that the SERIALIZED `line` fits within maxSize bytes. The serialized
43
+ * line is `overhead(empty message) + JSON-escaped bytes of the message`, so we measure the
44
+ * message's escaped size directly (single pass) instead of repeatedly re-serializing the whole
45
+ * line. This is exact for JSON.stringify's escaping and avoids the cost of a binary search.
46
+ */
47
+ function truncateMessageToFitLine(line, message, maxSize) {
48
+ var _a;
49
+ const overhead = utf8ByteLength(JSON.stringify(Object.assign(Object.assign({}, line), { message: '' })));
50
+ const totalRawBytes = utf8ByteLength(message);
51
+ // Marker "...[truncated N bytes]" is escape-free ASCII, so its serialized size equals its
52
+ // raw size = 21 + digits(N). Reserve for the max possible N so the final line never overflows.
53
+ const MARKER_STATIC_BYTES = 21;
54
+ const reservedMarkerBytes = MARKER_STATIC_BYTES + String(totalRawBytes).length;
55
+ const contentBudget = maxSize - overhead - reservedMarkerBytes;
56
+ if (contentBudget <= 0) {
57
+ return '';
58
+ }
59
+ // Keep whole code points until the escaped budget is exhausted (never splits a character).
60
+ let keptUnits = 0;
61
+ let keptEscapedBytes = 0;
62
+ let keptRawBytes = 0;
63
+ for (let i = 0; i < message.length;) {
64
+ const code = (_a = message.codePointAt(i)) !== null && _a !== void 0 ? _a : 0;
65
+ const escapedBytes = jsonEscapedByteSize(code);
66
+ if (keptEscapedBytes + escapedBytes > contentBudget) {
67
+ break;
68
+ }
69
+ keptEscapedBytes += escapedBytes;
70
+ keptRawBytes += codePointByteSize(code);
71
+ const units = code > 0xffff ? 2 : 1;
72
+ i += units;
73
+ keptUnits += units;
74
+ }
75
+ if (keptRawBytes >= totalRawBytes) {
76
+ return message;
77
+ }
78
+ const removed = totalRawBytes - keptRawBytes;
79
+ return `${message.slice(0, keptUnits)}...[truncated ${removed} bytes]`;
80
+ }
81
+ /**
82
+ * Serializes a log line while enforcing the per-line byte limit on the *serialized* line — what
83
+ * the server measures — covering the message, parameters and metadata plus JSON-escaping
84
+ * overhead. Returns the JSON string for the line (reused to build the packet, so each line is
85
+ * serialized only once). Oversized `parameters` (structured data we can't safely truncate
86
+ * mid-JSON) are replaced with a size marker; the message is then truncated to fit the remainder.
87
+ */
88
+ function serializeLineWithinByteLimit(line, maxSize) {
89
+ var _a;
90
+ const serialized = JSON.stringify(line);
91
+ // Cheap fast path: at most 3 UTF-8 bytes per UTF-16 code unit, so if 3 * length fits the
92
+ // line is definitely under the limit and we avoid the exact byte count entirely.
93
+ if (serialized.length * 3 <= maxSize || utf8ByteLength(serialized) <= maxSize) {
94
+ return serialized;
95
+ }
96
+ const result = Object.assign({}, line);
97
+ // If the line is over the limit even with an empty message, the bulk is in `parameters` —
98
+ // replace it with a marker so the (human-readable) message is what we keep room for.
99
+ if (utf8ByteLength(JSON.stringify(Object.assign(Object.assign({}, result), { message: '' }))) > maxSize) {
100
+ const parametersByteSize = utf8ByteLength(JSON.stringify((_a = result.parameters) !== null && _a !== void 0 ? _a : ''));
101
+ result.parameters = { truncated: true, originalByteSize: parametersByteSize };
102
+ }
103
+ result.message = truncateMessageToFitLine(result, line.message, maxSize);
104
+ return JSON.stringify(result);
105
+ }
7
106
  export default class Logger {
8
107
  constructor({ serverLoggingCallback, isDebug, clientLoggingCallback, maxLogLinesBeforeFlush, getContextEmail }) {
9
108
  // An array of log lines that limits itself to a certain number of entries (deleting the oldest)
@@ -29,16 +128,20 @@ export default class Logger {
29
128
  if (!this.logLines.length || ((_a = this.logLines) === null || _a === void 0 ? void 0 : _a.every((l) => l.onlyFlushWithOthers))) {
30
129
  return;
31
130
  }
32
- // We don't care about log setting web cookies so let's define it as false
33
- const linesToLog = (_b = this.logLines) === null || _b === void 0 ? void 0 : _b.map((l) => {
131
+ // We don't care about log setting web cookies so let's define it as false.
132
+ // Serialize each line while bounding it to the server's per-line size limit (covers
133
+ // message, parameters and JSON-escaping overhead). Building the packet by joining the
134
+ // per-line JSON keeps each line serialized only once — identical output to
135
+ // JSON.stringify(array) with no extra pass.
136
+ const serializedLines = (_b = this.logLines) === null || _b === void 0 ? void 0 : _b.map((l) => {
34
137
  // eslint-disable-next-line no-param-reassign
35
138
  delete l.onlyFlushWithOthers;
36
- return l;
139
+ return serializeLineWithinByteLimit(l, MAX_LOG_LINE_BYTES);
37
140
  });
38
141
  this.logLines = [];
39
142
  const promise = this.serverLoggingCallback(this, {
40
143
  api_setCookie: false,
41
- logPacket: JSON.stringify(linesToLog),
144
+ logPacket: `[${serializedLines.join(',')}]`,
42
145
  });
43
146
  if (!promise) {
44
147
  return;
@@ -66,13 +169,8 @@ export default class Logger {
66
169
  catch (_a) {
67
170
  // Silently fail if getContextEmail throws - logging should not crash
68
171
  }
69
- let cappedMessage = message;
70
- if (message.length > MAX_LOG_MESSAGE_LENGTH) {
71
- const omittedCount = message.length - MAX_LOG_MESSAGE_LENGTH;
72
- cappedMessage = `${message.slice(0, MAX_LOG_MESSAGE_LENGTH)}…[truncated ${omittedCount} characters]`;
73
- }
74
172
  const length = this.logLines.push({
75
- message: cappedMessage,
173
+ message,
76
174
  parameters,
77
175
  onlyFlushWithOthers,
78
176
  timestamp: new Date(),
@@ -146,3 +244,5 @@ export default class Logger {
146
244
  this.clientLoggingCallback(message, extraData);
147
245
  }
148
246
  }
247
+ // Exported for unit testing.
248
+ export { truncateMessageToFitLine, serializeLineWithinByteLimit, utf8ByteLength, MAX_LOG_LINE_BYTES };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expensify-common",
3
- "version": "2.0.190",
3
+ "version": "2.0.192",
4
4
  "author": "Expensify, Inc.",
5
5
  "description": "Expensify libraries and components shared across different repos",
6
6
  "homepage": "https://expensify.com",
@@ -304,6 +304,11 @@
304
304
  "typescript": "^5.7.2",
305
305
  "typescript-eslint": "^8.61.0"
306
306
  },
307
+ "overrides": {
308
+ "minimatch@3": {
309
+ "brace-expansion": "1.1.12"
310
+ }
311
+ },
307
312
  "browserify": {
308
313
  "transform": [
309
314
  [