@optimizely-opal/opal-tool-ocp-sdk 1.0.0-beta.9 → 1.0.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.
@@ -88,6 +88,30 @@ describe('ToolLogger', () => {
88
88
  return response;
89
89
  };
90
90
 
91
+ const createMockResponseWithBody = (
92
+ status: number,
93
+ bodyData: Uint8Array | undefined,
94
+ contentType: string
95
+ ): App.Response => {
96
+ const mockHeaders = {
97
+ get: jest.fn().mockReturnValue(contentType)
98
+ };
99
+
100
+ const response = {
101
+ status,
102
+ headers: mockHeaders
103
+ } as any;
104
+
105
+ Object.defineProperty(response, 'bodyAsU8Array', {
106
+ get() {
107
+ return bodyData;
108
+ },
109
+ enumerable: true
110
+ });
111
+
112
+ return response;
113
+ };
114
+
91
115
  describe('logRequest', () => {
92
116
  it('should log request with parameters', () => {
93
117
  const req = createMockRequest();
@@ -240,7 +264,7 @@ describe('ToolLogger', () => {
240
264
  path: '/test-tool',
241
265
  method: 'POST',
242
266
  parameters: {
243
- description: `${'a'.repeat(100)}... (truncated, 150 chars total)`,
267
+ description: `${'a'.repeat(118)}...[22 truncated]...${'a'.repeat(10)}`,
244
268
  short_field: 'normal'
245
269
  }
246
270
  });
@@ -265,8 +289,8 @@ describe('ToolLogger', () => {
265
289
  method: 'POST',
266
290
  parameters: {
267
291
  items: [
268
- ...largeArray.slice(0, 10),
269
- '... (5 more items truncated)'
292
+ ...largeArray.slice(0, 2),
293
+ '... (13 more items truncated)'
270
294
  ],
271
295
  small_array: ['a', 'b']
272
296
  }
@@ -420,6 +444,21 @@ describe('ToolLogger', () => {
420
444
  // Should not throw error or cause infinite recursion
421
445
  expect(() => ToolLogger.logRequest(req)).not.toThrow();
422
446
  expect(mockLogger.info).toHaveBeenCalled();
447
+
448
+ // Verify that deeply nested parts are replaced with placeholder
449
+ const logCall = mockLogger.info.mock.calls[0];
450
+ const loggedData = JSON.parse(logCall[1]);
451
+
452
+ // Navigate to a deeply nested level that should be truncated
453
+ // At maxDepth=5, objects beyond depth 5 should be replaced
454
+ const nested1 = loggedData.parameters.deep.nested;
455
+ expect(nested1).toBeDefined(); // depth 2
456
+ const nested2 = nested1.nested;
457
+ expect(nested2).toBeDefined(); // depth 3
458
+ const nested3 = nested2.nested;
459
+ expect(nested3).toBeDefined(); // depth 4
460
+ const nested4 = nested3.nested;
461
+ expect(nested4).toBe('[MAX_DEPTH_EXCEEDED]'); // depth 5, should be truncated
423
462
  });
424
463
 
425
464
  it('should replace deeply nested objects with MAX_DEPTH_EXCEEDED placeholder', () => {
@@ -465,6 +504,7 @@ describe('ToolLogger', () => {
465
504
  level1: {
466
505
  items: [
467
506
  {
507
+ shallow: 'data',
468
508
  level2: {
469
509
  level3: {
470
510
  level4: {
@@ -504,9 +544,12 @@ describe('ToolLogger', () => {
504
544
  const loggedData = JSON.parse(logCall[1]);
505
545
 
506
546
  const items = loggedData.parameters.level1.items;
547
+ expect(items.length).toBe(3);
507
548
 
508
549
  // First item: deeply nested object with inner parts replaced by placeholder
550
+ // shallow object should be processed normally
509
551
  expect(items[0]).toEqual({
552
+ shallow: 'data',
510
553
  level2: {
511
554
  level3: {
512
555
  level4: '[MAX_DEPTH_EXCEEDED]'
@@ -516,18 +559,6 @@ describe('ToolLogger', () => {
516
559
 
517
560
  // Second item: simple string should remain unchanged
518
561
  expect(items[1]).toBe('simple-item');
519
-
520
- // Third item: shallow object should be processed normally
521
- expect(items[2]).toEqual({ shallow: 'data' });
522
-
523
- // Fourth item: another deeply nested object with inner parts replaced by placeholder
524
- expect(items[3]).toEqual({
525
- level2: {
526
- level3: {
527
- level4: '[MAX_DEPTH_EXCEEDED]'
528
- }
529
- }
530
- });
531
562
  });
532
563
  });
533
564
 
@@ -545,7 +576,8 @@ describe('ToolLogger', () => {
545
576
  status: 200,
546
577
  contentType: 'application/json',
547
578
  contentLength: 34, // JSON.stringify({ result: 'success', data: 'test' }).length
548
- success: true
579
+ success: true,
580
+ responseBody: { result: 'success', data: 'test' }
549
581
  };
550
582
 
551
583
  expect(mockLogger.info).toHaveBeenCalledWith(
@@ -567,14 +599,14 @@ describe('ToolLogger', () => {
567
599
  status: 400,
568
600
  contentType: 'application/json',
569
601
  contentLength: 23,
570
- success: false
602
+ success: false,
603
+ responseBody: { error: 'Bad request' }
571
604
  });
572
605
  });
573
606
 
574
- it('should handle response without bodyJSON', () => {
607
+ it('should handle response without body data', () => {
575
608
  const req = createMockRequest();
576
- const response = createMockResponse(204);
577
- response.bodyJSON = undefined;
609
+ const response = createMockResponseWithBody(204, undefined, 'application/json');
578
610
 
579
611
  ToolLogger.logResponse(req, response);
580
612
 
@@ -600,11 +632,12 @@ describe('ToolLogger', () => {
600
632
  status: 200,
601
633
  contentType: 'application/json',
602
634
  contentLength: 15,
603
- success: true
635
+ success: true,
636
+ responseBody: { data: 'test' }
604
637
  });
605
638
  });
606
639
 
607
- it('should handle unknown content type', () => {
640
+ it('should handle unknown content type - response body not logged', () => {
608
641
  const req = createMockRequest();
609
642
  const response = createMockResponse(200, { data: 'test' });
610
643
  response.headers.get = jest.fn().mockReturnValue(null);
@@ -623,21 +656,35 @@ describe('ToolLogger', () => {
623
656
 
624
657
  it('should handle content length calculation error', () => {
625
658
  const req = createMockRequest();
626
- const circularObj: any = { name: 'test' };
627
- circularObj.self = circularObj; // Create circular reference
628
659
 
629
- const response = createMockResponse(200, circularObj);
630
-
631
- ToolLogger.logResponse(req, response);
660
+ // Simulate a response that will cause errors when trying to calculate content length
661
+ // by providing a Uint8Array but the underlying data causes issues
662
+ const mockHeaders = {
663
+ get: jest.fn().mockReturnValue('application/json')
664
+ };
632
665
 
633
- expectJsonLog({
634
- event: 'opal_tool_response',
635
- path: '/test-tool',
666
+ const response = {
636
667
  status: 200,
637
- contentType: 'application/json',
638
- contentLength: 'unknown',
639
- success: true
668
+ headers: mockHeaders
669
+ } as any;
670
+
671
+ // Create a getter that throws when accessed (simulating serialization error)
672
+ Object.defineProperty(response, 'bodyAsU8Array', {
673
+ get() {
674
+ throw new Error('Circular structure');
675
+ },
676
+ enumerable: true
640
677
  });
678
+
679
+ ToolLogger.logResponse(req, response);
680
+
681
+ // The error causes both contentLength and responseBody to fail gracefully
682
+ const logCall = mockLogger.info.mock.calls[0];
683
+ const loggedData = JSON.parse(logCall[1]);
684
+
685
+ expect(loggedData.event).toBe('opal_tool_response');
686
+ expect(loggedData.contentLength).toBe('unknown');
687
+ expect(loggedData.responseBody).toBeUndefined();
641
688
  });
642
689
 
643
690
  it('should correctly identify success status codes', () => {
@@ -659,14 +706,16 @@ describe('ToolLogger', () => {
659
706
  const response = createMockResponse(status);
660
707
  ToolLogger.logResponse(req, response);
661
708
 
662
- expectJsonLog({
663
- event: 'opal_tool_response',
664
- path: '/test-tool',
665
- status,
666
- contentType: 'application/json',
667
- contentLength: 2,
668
- success: expected
669
- });
709
+ const logCall = mockLogger.info.mock.calls[0];
710
+ const loggedData = JSON.parse(logCall[1]);
711
+
712
+ expect(loggedData.event).toBe('opal_tool_response');
713
+ expect(loggedData.path).toBe('/test-tool');
714
+ expect(loggedData.status).toBe(status);
715
+ expect(loggedData.contentType).toBe('application/json');
716
+ expect(loggedData.contentLength).toBe(2);
717
+ expect(loggedData.success).toBe(expected);
718
+ expect(loggedData.responseBody).toEqual({});
670
719
  });
671
720
  });
672
721
 
@@ -674,29 +723,148 @@ describe('ToolLogger', () => {
674
723
  const req = createMockRequest();
675
724
 
676
725
  const testCases = [
677
- 'application/json',
678
- 'text/plain',
679
- 'application/xml',
680
- 'text/html'
726
+ { contentType: 'application/json', expectedBody: { data: 'test' } },
727
+ { contentType: 'text/plain', expectedBody: '{"data":"test"}' },
728
+ { contentType: 'text/html', expectedBody: '{"data":"test"}' }
681
729
  ];
682
730
 
683
- testCases.forEach((contentType) => {
731
+ testCases.forEach(({ contentType, expectedBody }) => {
684
732
  mockLogger.info.mockClear();
685
733
  const response = createMockResponse(200, { data: 'test' });
686
734
  response.headers.get = jest.fn().mockReturnValue(contentType);
687
735
 
688
736
  ToolLogger.logResponse(req, response);
689
737
 
690
- expectJsonLog({
691
- event: 'opal_tool_response',
692
- path: '/test-tool',
693
- status: 200,
694
- contentType,
695
- contentLength: 15,
696
- success: true
697
- });
738
+ const logCall = mockLogger.info.mock.calls[0];
739
+ const loggedData = JSON.parse(logCall[1]);
740
+
741
+ expect(loggedData.event).toBe('opal_tool_response');
742
+ expect(loggedData.path).toBe('/test-tool');
743
+ expect(loggedData.status).toBe(200);
744
+ expect(loggedData.contentType).toBe(contentType);
745
+ expect(loggedData.contentLength).toBe(15);
746
+ expect(loggedData.success).toBe(true);
747
+ expect(loggedData.responseBody).toEqual(expectedBody);
748
+ });
749
+ });
750
+
751
+ it('should log short successful response body', () => {
752
+ const req = createMockRequest();
753
+ const response = createMockResponse(200, { result: 'success', data: 'test' });
754
+
755
+ ToolLogger.logResponse(req, response);
756
+
757
+ const logCall = mockLogger.info.mock.calls[0];
758
+ const loggedData = JSON.parse(logCall[1]);
759
+
760
+ expect(loggedData.responseBody).toEqual({ result: 'success', data: 'test' });
761
+ expect(loggedData.success).toBe(true);
762
+ });
763
+
764
+ it('should truncate long successful response body to 256 chars', () => {
765
+ const req = createMockRequest();
766
+ const longData = 'a'.repeat(300);
767
+ const response = createMockResponse(200, { message: longData });
768
+
769
+ ToolLogger.logResponse(req, response);
770
+
771
+ const logCall = mockLogger.info.mock.calls[0];
772
+ const loggedData = JSON.parse(logCall[1]);
773
+
774
+ // The response body should be truncated when stringified
775
+ expect(loggedData.responseBody.message).toContain(' truncated]...');
776
+ });
777
+
778
+ it('truncates long properties of failed responses', () => {
779
+ const req = createMockRequest();
780
+ const longData = 'a'.repeat(150);
781
+ const response = createMockResponse(400, { error: 'Bad request', details: longData });
782
+
783
+ ToolLogger.logResponse(req, response);
784
+
785
+ const logCall = mockLogger.info.mock.calls[0];
786
+ const loggedData = JSON.parse(logCall[1]);
787
+
788
+ // Failed responses should include full body, not truncated
789
+ expect(loggedData.responseBody.error).toEqual('Bad request');
790
+ expect(loggedData.responseBody.details).toContain(' truncated]...');
791
+ expect(loggedData.success).toBe(false);
792
+ });
793
+
794
+ it('should redact sensitive data in response body', () => {
795
+ const req = createMockRequest();
796
+ const response = createMockResponse(200, {
797
+ user: 'john',
798
+ password: 'secret123',
799
+ api_key: 'key456',
800
+ data: 'public'
801
+ });
802
+
803
+ ToolLogger.logResponse(req, response);
804
+
805
+ const logCall = mockLogger.info.mock.calls[0];
806
+ const loggedData = JSON.parse(logCall[1]);
807
+
808
+ expect(loggedData.responseBody).toEqual({
809
+ user: 'john',
810
+ password: '[REDACTED]',
811
+ api_key: '[REDACTED]',
812
+ data: 'public'
698
813
  });
699
814
  });
815
+
816
+ it('should handle response with no body', () => {
817
+ const req = createMockRequest();
818
+ const response = createMockResponseWithBody(204, undefined, 'application/json');
819
+
820
+ ToolLogger.logResponse(req, response);
821
+
822
+ const logCall = mockLogger.info.mock.calls[0];
823
+ const loggedData = JSON.parse(logCall[1]);
824
+
825
+ expect(loggedData.responseBody).toBeUndefined();
826
+ expect(loggedData.success).toBe(true);
827
+ });
828
+
829
+ it('should handle plain text response body', () => {
830
+ const req = createMockRequest();
831
+ const plainText = 'This is a plain text response';
832
+ const response = createMockResponseWithBody(200, new Uint8Array(Buffer.from(plainText)), 'text/plain');
833
+
834
+ ToolLogger.logResponse(req, response);
835
+
836
+ const logCall = mockLogger.info.mock.calls[0];
837
+ const loggedData = JSON.parse(logCall[1]);
838
+
839
+ expect(loggedData.responseBody).toBe(plainText);
840
+ });
841
+
842
+ it('should truncate long plain text successful responses', () => {
843
+ const req = createMockRequest();
844
+ const longText = 'a'.repeat(300);
845
+ const response = createMockResponseWithBody(200, new Uint8Array(Buffer.from(longText)), 'text/plain');
846
+
847
+ ToolLogger.logResponse(req, response);
848
+
849
+ const logCall = mockLogger.info.mock.calls[0];
850
+ const loggedData = JSON.parse(logCall[1]);
851
+
852
+ expect(loggedData.responseBody).toBe(`${'a'.repeat(256)}... (truncated)`);
853
+ });
854
+
855
+ it('should not truncate long plain text failed responses', () => {
856
+ const req = createMockRequest();
857
+ const longText = 'a'.repeat(150);
858
+ const response = createMockResponseWithBody(500, new Uint8Array(Buffer.from(longText)), 'text/plain');
859
+
860
+ ToolLogger.logResponse(req, response);
861
+
862
+ const logCall = mockLogger.info.mock.calls[0];
863
+ const loggedData = JSON.parse(logCall[1]);
864
+
865
+ expect(loggedData.responseBody).toBe(longText);
866
+ expect(loggedData.success).toBe(false);
867
+ });
700
868
  });
701
869
 
702
870
  describe('edge cases', () => {
@@ -743,7 +911,7 @@ describe('ToolLogger', () => {
743
911
  string: 'text',
744
912
  number: 42,
745
913
  boolean: true,
746
- array: [1, 2, 3],
914
+ array: [1, 2],
747
915
  object: { nested: 'value' },
748
916
  nullValue: null,
749
917
  password: 'secret'
@@ -761,7 +929,7 @@ describe('ToolLogger', () => {
761
929
  string: 'text',
762
930
  number: 42,
763
931
  boolean: true,
764
- array: [1, 2, 3],
932
+ array: [1, 2],
765
933
  object: { nested: 'value' },
766
934
  nullValue: null,
767
935
  password: '[REDACTED]'
@@ -841,24 +1009,7 @@ describe('ToolLogger', () => {
841
1009
  description: 'OVERRIDDEN: Enhanced minimum detectable effect calculation',
842
1010
  required: true
843
1011
  },
844
- {
845
- name: 'sigLevel',
846
- type: 'number',
847
- description: 'OVERRIDDEN: Enhanced statistical significance level',
848
- required: true
849
- },
850
- {
851
- name: 'numVariations',
852
- type: 'number',
853
- description: 'OVERRIDDEN: Enhanced number of variations handling',
854
- required: true
855
- },
856
- {
857
- name: 'dailyVisitors',
858
- type: 'number',
859
- description: 'OVERRIDDEN: Enhanced daily visitor count with forecasting',
860
- required: true
861
- }
1012
+ '... (3 more items truncated)'
862
1013
  ]
863
1014
  }
864
1015
  ]
@@ -1,6 +1,10 @@
1
1
  import { logger, LogVisibility } from '@zaiusinc/app-sdk';
2
2
  import * as App from '@zaiusinc/app-sdk';
3
3
 
4
+ const MAX_PARAM_LOG_LENGTH = 128;
5
+ const MAX_BODY_LOG_LENGTH = 256;
6
+ const MAX_ARRAY_ITEMS = 2;
7
+
4
8
  /**
5
9
  * Utility class for logging Opal tool requests and responses with security considerations
6
10
  */
@@ -48,13 +52,13 @@ export class ToolLogger {
48
52
  'bearer_token'
49
53
  ];
50
54
 
51
- private static readonly MAX_PARAM_LENGTH = 100;
52
- private static readonly MAX_ARRAY_ITEMS = 10;
53
-
54
55
  /**
55
56
  * Redacts sensitive data from an object
56
57
  */
57
- private static redactSensitiveData(data: any, maxDepth = 5): any {
58
+ private static redactSensitiveDataAndTruncate(data: any, maxDepth = 5, accumulatedLength = 0): any {
59
+ if (accumulatedLength > MAX_BODY_LOG_LENGTH) {
60
+ return '';
61
+ }
58
62
  if (maxDepth <= 0) {
59
63
  return '[MAX_DEPTH_EXCEEDED]';
60
64
  }
@@ -64,9 +68,13 @@ export class ToolLogger {
64
68
  }
65
69
 
66
70
  if (typeof data === 'string') {
67
- return data.length > this.MAX_PARAM_LENGTH
68
- ? `${data.substring(0, this.MAX_PARAM_LENGTH)}... (truncated, ${data.length} chars total)`
69
- : data;
71
+ if (data.length > MAX_PARAM_LOG_LENGTH) {
72
+ const lead = data.substring(0, MAX_PARAM_LOG_LENGTH - 10);
73
+ const tail = data.substring(data.length - 10);
74
+ return `${lead}...[${data.length - MAX_PARAM_LOG_LENGTH} truncated]...${tail}`;
75
+ } else {
76
+ return data;
77
+ }
70
78
  }
71
79
 
72
80
  if (typeof data === 'number' || typeof data === 'boolean') {
@@ -74,10 +82,10 @@ export class ToolLogger {
74
82
  }
75
83
 
76
84
  if (Array.isArray(data)) {
77
- const truncated = data.slice(0, this.MAX_ARRAY_ITEMS);
78
- const result = truncated.map((item) => this.redactSensitiveData(item, maxDepth));
79
- if (data.length > this.MAX_ARRAY_ITEMS) {
80
- result.push(`... (${data.length - this.MAX_ARRAY_ITEMS} more items truncated)`);
85
+ const truncated = data.slice(0, MAX_ARRAY_ITEMS);
86
+ const result = truncated.map((item) => this.redactSensitiveDataAndTruncate(item, maxDepth, accumulatedLength));
87
+ if (data.length > MAX_ARRAY_ITEMS) {
88
+ result.push(`... (${data.length - MAX_ARRAY_ITEMS} more items truncated)`);
81
89
  }
82
90
  return result;
83
91
  }
@@ -85,13 +93,20 @@ export class ToolLogger {
85
93
  if (typeof data === 'object') {
86
94
  const result: any = {};
87
95
  for (const [key, value] of Object.entries(data)) {
96
+ if (accumulatedLength > MAX_BODY_LOG_LENGTH) {
97
+ break;
98
+ }
88
99
  // Check if this field contains sensitive data
89
100
  const isSensitive = this.isSensitiveField(key);
90
101
 
91
102
  if (isSensitive) {
92
103
  result[key] = '[REDACTED]';
93
104
  } else {
94
- result[key] = this.redactSensitiveData(value, maxDepth - 1);
105
+ result[key] = this.redactSensitiveDataAndTruncate(value, maxDepth - 1, accumulatedLength);
106
+ }
107
+
108
+ if (result[key]) {
109
+ accumulatedLength += JSON.stringify(result[key]).length;
95
110
  }
96
111
  }
97
112
  return result;
@@ -118,7 +133,7 @@ export class ToolLogger {
118
133
  return null;
119
134
  }
120
135
 
121
- return this.redactSensitiveData(params);
136
+ return this.redactSensitiveDataAndTruncate(params);
122
137
  }
123
138
 
124
139
  /**
@@ -136,6 +151,99 @@ export class ToolLogger {
136
151
  }
137
152
  }
138
153
 
154
+ /**
155
+ * Extracts the response body as a string or parsed JSON object
156
+ */
157
+ private static getResponseBody(response?: App.Response): any {
158
+ if (!response) {
159
+ return null;
160
+ }
161
+
162
+ try {
163
+ const contentType = response.headers?.get('content-type') || '';
164
+ const isJson = contentType.includes('application/json') || contentType.includes('application/problem+json');
165
+ const isText = contentType.startsWith('text/');
166
+
167
+ if (!isJson && !isText) {
168
+ return null;
169
+ }
170
+
171
+ // Try to access bodyAsU8Array - this may throw
172
+ const bodyData = response.bodyAsU8Array;
173
+ if (!bodyData) {
174
+ return null;
175
+ }
176
+
177
+ // Convert Uint8Array to string
178
+ const bodyString = Buffer.from(bodyData).toString();
179
+ if (!bodyString) {
180
+ return null;
181
+ }
182
+
183
+ // Try to parse as JSON if content-type indicates JSON
184
+ if (isJson) {
185
+ try {
186
+ return JSON.parse(bodyString);
187
+ } catch {
188
+ // If JSON parsing fails, return as string
189
+ return bodyString;
190
+ }
191
+ }
192
+
193
+ // Return as plain text for non-JSON content types
194
+ return bodyString;
195
+ } catch {
196
+ return null;
197
+ }
198
+ }
199
+
200
+ /**
201
+ * Creates a summary of response body with security redaction and truncation
202
+ * For failed responses (4xx, 5xx): returns full body with redacted sensitive data
203
+ * For successful responses (2xx): returns first 100 chars with redacted sensitive data
204
+ */
205
+ private static createResponseBodySummary(response?: App.Response, success?: boolean): any {
206
+ const body = this.getResponseBody(response);
207
+ if (body === null || body === undefined) {
208
+ return null;
209
+ }
210
+
211
+ // For objects (parsed JSON), apply redaction
212
+ if (typeof body === 'object') {
213
+ // For failed responses, don't truncate strings within the object
214
+ const redactedBody = this.redactSensitiveDataAndTruncate(body, 5);
215
+
216
+ // For successful responses, truncate to first MAX_BODY_LOG_LENGTH chars
217
+ if (success) {
218
+ const bodyString = JSON.stringify(redactedBody);
219
+ if (bodyString.length > MAX_BODY_LOG_LENGTH) {
220
+ const truncated = bodyString.substring(0, MAX_BODY_LOG_LENGTH);
221
+ return `${truncated}... (truncated)`;
222
+ }
223
+ return redactedBody;
224
+ }
225
+
226
+ // For failed responses, return full redacted body
227
+ return redactedBody;
228
+ }
229
+
230
+ // For strings (plain text or unparseable JSON)
231
+ if (typeof body === 'string') {
232
+ // For successful responses, truncate to first 100 chars
233
+ if (success) {
234
+ if (body.length > MAX_BODY_LOG_LENGTH) {
235
+ return `${body.substring(0, MAX_BODY_LOG_LENGTH)}... (truncated)`;
236
+ }
237
+ return body;
238
+ }
239
+
240
+ // For failed responses, return full body
241
+ return body;
242
+ }
243
+
244
+ return body;
245
+ }
246
+
139
247
  /**
140
248
  * Logs an incoming request
141
249
  */
@@ -162,16 +270,22 @@ export class ToolLogger {
162
270
  response: App.Response,
163
271
  processingTimeMs?: number
164
272
  ): void {
165
- const responseLog = {
273
+ const success = response.status >= 200 && response.status < 300;
274
+ const responseLog: any = {
166
275
  event: 'opal_tool_response',
167
276
  path: req.path,
168
277
  duration: processingTimeMs ? `${processingTimeMs}ms` : undefined,
169
278
  status: response.status,
170
279
  contentType: response.headers?.get('content-type') || 'unknown',
171
280
  contentLength: this.calculateContentLength(response),
172
- success: response.status >= 200 && response.status < 300
281
+ success
173
282
  };
174
283
 
284
+ const responseBodySummary = this.createResponseBodySummary(response, success);
285
+ if (responseBodySummary) {
286
+ responseLog.responseBody = responseBodySummary;
287
+ }
288
+
175
289
  // Log with Zaius audience so developers only see requests for accounts they have access to
176
290
  logger.info(LogVisibility.Zaius, JSON.stringify(responseLog));
177
291
  }