expensify-common 2.0.189 → 2.0.191

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,6 +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;
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
+ }
4
113
  class Logger {
5
114
  constructor({ serverLoggingCallback, isDebug, clientLoggingCallback, maxLogLinesBeforeFlush, getContextEmail }) {
6
115
  // An array of log lines that limits itself to a certain number of entries (deleting the oldest)
@@ -26,16 +135,20 @@ class Logger {
26
135
  if (!this.logLines.length || ((_a = this.logLines) === null || _a === void 0 ? void 0 : _a.every((l) => l.onlyFlushWithOthers))) {
27
136
  return;
28
137
  }
29
- // We don't care about log setting web cookies so let's define it as false
30
- 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) => {
31
144
  // eslint-disable-next-line no-param-reassign
32
145
  delete l.onlyFlushWithOthers;
33
- return l;
146
+ return serializeLineWithinByteLimit(l, MAX_LOG_LINE_BYTES);
34
147
  });
35
148
  this.logLines = [];
36
149
  const promise = this.serverLoggingCallback(this, {
37
150
  api_setCookie: false,
38
- logPacket: JSON.stringify(linesToLog),
151
+ logPacket: `[${serializedLines.join(',')}]`,
39
152
  });
40
153
  if (!promise) {
41
154
  return;
@@ -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,4 +1,108 @@
1
1
  const MAX_LOG_LINES_BEFORE_FLUSH = 50;
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
+ }
2
106
  export default class Logger {
3
107
  constructor({ serverLoggingCallback, isDebug, clientLoggingCallback, maxLogLinesBeforeFlush, getContextEmail }) {
4
108
  // An array of log lines that limits itself to a certain number of entries (deleting the oldest)
@@ -24,16 +128,20 @@ export default class Logger {
24
128
  if (!this.logLines.length || ((_a = this.logLines) === null || _a === void 0 ? void 0 : _a.every((l) => l.onlyFlushWithOthers))) {
25
129
  return;
26
130
  }
27
- // We don't care about log setting web cookies so let's define it as false
28
- 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) => {
29
137
  // eslint-disable-next-line no-param-reassign
30
138
  delete l.onlyFlushWithOthers;
31
- return l;
139
+ return serializeLineWithinByteLimit(l, MAX_LOG_LINE_BYTES);
32
140
  });
33
141
  this.logLines = [];
34
142
  const promise = this.serverLoggingCallback(this, {
35
143
  api_setCookie: false,
36
- logPacket: JSON.stringify(linesToLog),
144
+ logPacket: `[${serializedLines.join(',')}]`,
37
145
  });
38
146
  if (!promise) {
39
147
  return;
@@ -136,3 +244,5 @@ export default class Logger {
136
244
  this.clientLoggingCallback(message, extraData);
137
245
  }
138
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.189",
3
+ "version": "2.0.191",
4
4
  "author": "Expensify, Inc.",
5
5
  "description": "Expensify libraries and components shared across different repos",
6
6
  "homepage": "https://expensify.com",