@elastic/synthetics 1.4.0 → 1.5.1

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.
Files changed (97) hide show
  1. package/dist/bundles/lib/index.js +432 -0
  2. package/dist/cli.js +3 -3
  3. package/dist/cli.js.map +1 -1
  4. package/dist/common_types.d.ts +12 -2
  5. package/dist/common_types.d.ts.map +1 -1
  6. package/dist/config.d.ts +1 -1
  7. package/dist/config.d.ts.map +1 -1
  8. package/dist/config.js +7 -1
  9. package/dist/config.js.map +1 -1
  10. package/dist/core/browser-service.js +2 -2
  11. package/dist/core/browser-service.js.map +1 -1
  12. package/dist/core/expect.d.ts +1 -152
  13. package/dist/core/expect.d.ts.map +1 -1
  14. package/dist/core/expect.js +14 -5
  15. package/dist/core/expect.js.map +1 -1
  16. package/dist/core/gatherer.d.ts +1 -1
  17. package/dist/core/gatherer.d.ts.map +1 -1
  18. package/dist/core/gatherer.js +5 -5
  19. package/dist/core/gatherer.js.map +1 -1
  20. package/dist/core/runner.d.ts.map +1 -1
  21. package/dist/core/runner.js +8 -7
  22. package/dist/core/runner.js.map +1 -1
  23. package/dist/dsl/journey.d.ts +1 -1
  24. package/dist/dsl/journey.d.ts.map +1 -1
  25. package/dist/dsl/monitor.d.ts +4 -0
  26. package/dist/dsl/monitor.d.ts.map +1 -1
  27. package/dist/dsl/monitor.js.map +1 -1
  28. package/dist/helpers.d.ts +0 -5
  29. package/dist/helpers.d.ts.map +1 -1
  30. package/dist/helpers.js +1 -24
  31. package/dist/helpers.js.map +1 -1
  32. package/dist/index.d.ts +1 -1
  33. package/dist/index.d.ts.map +1 -1
  34. package/dist/options.d.ts +2 -1
  35. package/dist/options.d.ts.map +1 -1
  36. package/dist/options.js +13 -4
  37. package/dist/options.js.map +1 -1
  38. package/dist/plugins/browser-console.d.ts +3 -2
  39. package/dist/plugins/browser-console.d.ts.map +1 -1
  40. package/dist/plugins/browser-console.js +33 -7
  41. package/dist/plugins/browser-console.js.map +1 -1
  42. package/dist/plugins/network.d.ts +1 -0
  43. package/dist/plugins/network.d.ts.map +1 -1
  44. package/dist/plugins/network.js +15 -2
  45. package/dist/plugins/network.js.map +1 -1
  46. package/dist/push/kibana_api.d.ts.map +1 -1
  47. package/dist/push/kibana_api.js +1 -1
  48. package/dist/push/kibana_api.js.map +1 -1
  49. package/dist/push/monitor.d.ts.map +1 -1
  50. package/dist/push/monitor.js +10 -2
  51. package/dist/push/monitor.js.map +1 -1
  52. package/dist/push/request.d.ts.map +1 -1
  53. package/dist/push/request.js +4 -3
  54. package/dist/push/request.js.map +1 -1
  55. package/dist/push/utils.js +1 -1
  56. package/dist/push/utils.js.map +1 -1
  57. package/dist/reporters/base.d.ts +3 -0
  58. package/dist/reporters/base.d.ts.map +1 -1
  59. package/dist/reporters/base.js +20 -22
  60. package/dist/reporters/base.js.map +1 -1
  61. package/dist/reporters/index.d.ts +1 -0
  62. package/dist/reporters/index.d.ts.map +1 -1
  63. package/dist/reporters/index.js.map +1 -1
  64. package/dist/reporters/json.d.ts.map +1 -1
  65. package/dist/reporters/json.js +34 -1
  66. package/dist/reporters/json.js.map +1 -1
  67. package/dist/reporters/junit.d.ts.map +1 -1
  68. package/dist/reporters/junit.js +4 -4
  69. package/dist/reporters/junit.js.map +1 -1
  70. package/dist/reporters/reporter-util.d.ts +35 -0
  71. package/dist/reporters/reporter-util.d.ts.map +1 -0
  72. package/dist/reporters/reporter-util.js +192 -0
  73. package/dist/reporters/reporter-util.js.map +1 -0
  74. package/package.json +16 -8
  75. package/src/cli.ts +3 -3
  76. package/src/common_types.ts +15 -2
  77. package/src/config.ts +12 -1
  78. package/src/core/browser-service.ts +1 -1
  79. package/src/core/expect.ts +13 -191
  80. package/src/core/gatherer.ts +1 -1
  81. package/src/core/runner.ts +15 -9
  82. package/src/dsl/journey.ts +1 -1
  83. package/src/dsl/monitor.ts +4 -0
  84. package/src/helpers.ts +0 -24
  85. package/src/index.ts +1 -1
  86. package/src/options.ts +16 -4
  87. package/src/plugins/browser-console.ts +39 -8
  88. package/src/plugins/network.ts +16 -3
  89. package/src/push/kibana_api.ts +4 -3
  90. package/src/push/monitor.ts +13 -2
  91. package/src/push/request.ts +12 -4
  92. package/src/push/utils.ts +1 -1
  93. package/src/reporters/base.ts +26 -33
  94. package/src/reporters/index.ts +5 -1
  95. package/src/reporters/json.ts +39 -3
  96. package/src/reporters/junit.ts +5 -5
  97. package/src/reporters/reporter-util.ts +217 -0
@@ -29,7 +29,7 @@ import {
29
29
  BrowserContext,
30
30
  CDPSession,
31
31
  APIRequestContext,
32
- } from 'playwright-chromium';
32
+ } from 'playwright-core';
33
33
  import { Step } from './step';
34
34
  import { VoidCallback, HooksCallback, Params, Location } from '../common_types';
35
35
  import { Monitor, MonitorConfig } from './monitor';
@@ -72,6 +72,10 @@ export type MonitorConfig = {
72
72
  params?: Params;
73
73
  playwrightOptions?: PlaywrightOptions;
74
74
  alert?: AlertConfig;
75
+ /**
76
+ * By default, the monitor will be retested on failure
77
+ */
78
+ retestOnFailure?: boolean;
75
79
  };
76
80
 
77
81
  type MonitorFilter = {
package/src/helpers.ts CHANGED
@@ -234,30 +234,6 @@ export function rewriteErrorStack(stack: string, indexes: [number, number]) {
234
234
  return stack;
235
235
  }
236
236
 
237
- // formatError prefers receiving proper Errors, but since at runtime
238
- // non Error exceptions can be thrown, it tolerates though. The
239
- // redundant type Error | any expresses that.
240
- export function formatError(error: Error | any) {
241
- if (error === undefined || error === null) {
242
- return;
243
- }
244
-
245
- if (!(error instanceof Error)) {
246
- return {
247
- message: `Error "${error}" received, with type "${typeof error}". (Do not throw exceptions without using \`new Error("my message")\`)`,
248
- name: '',
249
- stack: '',
250
- };
251
- }
252
- const { name, message, stack } = error;
253
- const indexes = findPWLogsIndexes(message);
254
- return {
255
- name,
256
- message: rewriteErrorMessage(message, indexes[0]),
257
- stack: rewriteErrorStack(stack, indexes),
258
- };
259
- }
260
-
261
237
  const cwd = process.cwd();
262
238
  /**
263
239
  * All the settings that are related to the Synthetics is stored
package/src/index.ts CHANGED
@@ -55,7 +55,7 @@ export type {
55
55
  CDPSession,
56
56
  APIRequestContext,
57
57
  devices,
58
- } from 'playwright-chromium';
58
+ } from 'playwright-core';
59
59
 
60
60
  /**
61
61
  * Export the types necessary to write custom reporters
package/src/options.ts CHANGED
@@ -31,10 +31,10 @@ import { THROTTLING_WARNING_MSG, error, warn } from './helpers';
31
31
 
32
32
  type Mode = 'run' | 'push';
33
33
 
34
- export function normalizeOptions(
34
+ export async function normalizeOptions(
35
35
  cliArgs: CliArgs,
36
36
  mode: Mode = 'run'
37
- ): RunOptions {
37
+ ): Promise<RunOptions> {
38
38
  const options: RunOptions = {
39
39
  ...cliArgs,
40
40
  environment: process.env['NODE_ENV'] || 'development',
@@ -85,7 +85,7 @@ export function normalizeOptions(
85
85
  */
86
86
  const config =
87
87
  cliArgs.config || !cliArgs.inline
88
- ? readConfig(options.environment, cliArgs.config)
88
+ ? await readConfig(options.environment, cliArgs.config)
89
89
  : {};
90
90
 
91
91
  /**
@@ -106,7 +106,7 @@ export function normalizeOptions(
106
106
  );
107
107
  options.playwrightOptions = {
108
108
  ...playwrightOpts,
109
- headless: cliArgs.headless ?? playwrightOpts?.headless,
109
+ headless: getHeadlessFlag(cliArgs.headless, playwrightOpts?.headless),
110
110
  chromiumSandbox: cliArgs.sandbox ?? playwrightOpts?.chromiumSandbox,
111
111
  ignoreHTTPSErrors:
112
112
  cliArgs.ignoreHttpsErrors ?? playwrightOpts?.ignoreHTTPSErrors,
@@ -140,6 +140,18 @@ export function normalizeOptions(
140
140
  return options;
141
141
  }
142
142
 
143
+ export function getHeadlessFlag(
144
+ cliHeadless: boolean,
145
+ configHeadless?: boolean
146
+ ) {
147
+ // if cliHeadless is false, then we don't care about configHeadless
148
+ if (!cliHeadless) {
149
+ return false;
150
+ }
151
+ // default is headless
152
+ return configHeadless ?? true;
153
+ }
154
+
143
155
  export function validatePushOptions(opts: PushOptions) {
144
156
  if (opts.tags || opts.match) {
145
157
  throw error(`Aborted. Invalid CLI flags.
@@ -23,13 +23,16 @@
23
23
  *
24
24
  */
25
25
 
26
- import type { ConsoleMessage } from 'playwright-core';
27
- import { BrowserMessage, Driver } from '../common_types';
26
+ import type { ConsoleMessage, WebError } from 'playwright-core';
27
+ import { BrowserMessage, Driver, StatusValue } from '../common_types';
28
28
  import { log } from '../core/logger';
29
29
  import { Step } from '../dsl';
30
30
  import { getTimestamp } from '../helpers';
31
31
 
32
- const defaultMessageLimit = 1000;
32
+ const DEFAULT_MSG_LIMIt = 1000;
33
+ const SUCCESSFUL_MSG_LIMIT = 100;
34
+
35
+ const allowedTypes = ['error', 'warning', 'log'];
33
36
 
34
37
  export class BrowserConsole {
35
38
  private messages: BrowserMessage[] = [];
@@ -42,7 +45,7 @@ export class BrowserConsole {
42
45
  return;
43
46
  }
44
47
  const type = msg.type();
45
- if (type === 'error' || type === 'warning') {
48
+ if (allowedTypes.includes(type)) {
46
49
  const { name, index } = this._currentStep;
47
50
  this.messages.push({
48
51
  timestamp: getTimestamp(),
@@ -55,11 +58,12 @@ export class BrowserConsole {
55
58
  }
56
59
  };
57
60
 
58
- private pageErrorEventListener = (error: Error) => {
61
+ private webErrorListener = (webErr: WebError) => {
59
62
  if (!this._currentStep) {
60
63
  return;
61
64
  }
62
65
  const { name, index } = this._currentStep;
66
+ const error = webErr.error();
63
67
  this.messages.push({
64
68
  timestamp: getTimestamp(),
65
69
  text: error.message,
@@ -72,7 +76,7 @@ export class BrowserConsole {
72
76
  };
73
77
 
74
78
  private enforceMessagesLimit() {
75
- if (this.messages.length > defaultMessageLimit) {
79
+ if (this.messages.length > DEFAULT_MSG_LIMIt) {
76
80
  this.messages.splice(0, 1);
77
81
  }
78
82
  }
@@ -80,13 +84,40 @@ export class BrowserConsole {
80
84
  start() {
81
85
  log(`Plugins: started collecting console events`);
82
86
  this.driver.context.on('console', this.consoleEventListener);
83
- this.driver.page.on('pageerror', this.pageErrorEventListener);
87
+ this.driver.context.on('weberror', this.webErrorListener);
84
88
  }
85
89
 
86
90
  stop() {
87
91
  this.driver.context.off('console', this.consoleEventListener);
88
- this.driver.page.off('pageerror', this.pageErrorEventListener);
92
+ this.driver.context.off('weberror', this.webErrorListener);
89
93
  log(`Plugins: stopped collecting console events`);
90
94
  return this.messages;
91
95
  }
92
96
  }
97
+
98
+ export function filterBrowserMessages(
99
+ messages: BrowserMessage[],
100
+ status: StatusValue
101
+ ) {
102
+ if (status == 'skipped') {
103
+ return [];
104
+ } else if (status === 'failed' || messages.length <= SUCCESSFUL_MSG_LIMIT) {
105
+ return messages;
106
+ }
107
+ // collect 100 messages from the browser console when the test is successful,
108
+ // giving priority to errors and warnings
109
+ const result = messages.filter(msg => msg.type === 'error');
110
+ if (result.length >= SUCCESSFUL_MSG_LIMIT) {
111
+ return result.slice(-SUCCESSFUL_MSG_LIMIT);
112
+ }
113
+
114
+ // collect warnings
115
+ result.push(...messages.filter(msg => msg.type === 'warning'));
116
+ if (result.length >= SUCCESSFUL_MSG_LIMIT) {
117
+ return result.slice(-SUCCESSFUL_MSG_LIMIT);
118
+ }
119
+
120
+ // collect logs
121
+ result.push(...messages.filter(msg => msg.type === 'log'));
122
+ return result.slice(-SUCCESSFUL_MSG_LIMIT);
123
+ }
@@ -23,7 +23,7 @@
23
23
  *
24
24
  */
25
25
 
26
- import { Page, Request, Response } from 'playwright-chromium';
26
+ import { Frame, Page, Request, Response } from 'playwright-core';
27
27
  import { NetworkInfo, BrowserInfo, Driver } from '../common_types';
28
28
  import { log } from '../core/logger';
29
29
  import { Step } from '../dsl';
@@ -93,6 +93,17 @@ export class NetworkManager {
93
93
  race.then(() => this._barrierPromises.delete(race));
94
94
  }
95
95
 
96
+ private _nullableFrameBarrier(req: Request): Frame | null {
97
+ try {
98
+ return req.frame();
99
+ } catch (_) {
100
+ // frame might be unavailable for certain requests if they are issued
101
+ // before the frame is created - true for navigation requests
102
+ // https://playwright.dev/docs/api/class-request#request-frame
103
+ }
104
+ return null;
105
+ }
106
+
96
107
  async start() {
97
108
  log(`Plugins: started collecting network events`);
98
109
  const { client, context } = this.driver;
@@ -226,7 +237,8 @@ export class NetworkManager {
226
237
  };
227
238
  this._calcTotalTime(networkEntry, timing);
228
239
 
229
- const page = request.frame().page();
240
+ const frame = this._nullableFrameBarrier(request);
241
+ const page = frame?.page();
230
242
  this._addBarrier(
231
243
  page,
232
244
  request.allHeaders().then(reqHeaders => {
@@ -278,7 +290,8 @@ export class NetworkManager {
278
290
  return;
279
291
  }
280
292
 
281
- const page = request.frame().page();
293
+ const frame = this._nullableFrameBarrier(request);
294
+ const page = frame?.page();
282
295
  this._addBarrier(
283
296
  page,
284
297
  request.sizes().then(sizes => {
@@ -124,8 +124,8 @@ export async function bulkDeleteMonitors(
124
124
  }
125
125
 
126
126
  type StatusResponse = {
127
- version: {
128
- number: string;
127
+ kibana: {
128
+ version: string;
129
129
  };
130
130
  };
131
131
 
@@ -135,7 +135,8 @@ export async function getVersion(options: PushOptions) {
135
135
  method: 'GET',
136
136
  auth: options.auth,
137
137
  });
138
- return data.version.number;
138
+
139
+ return data.kibana.version;
139
140
  }
140
141
 
141
142
  export type LegacyAPISchema = {
@@ -242,12 +242,15 @@ export function buildMonitorFromYaml(
242
242
  config['private_locations'] ||
243
243
  config.privateLocations ||
244
244
  options.privateLocations;
245
- delete config['private_locations'];
245
+ const retestOnFailure =
246
+ config['retest_on_failure'] ?? options.retestOnFailure;
246
247
  const alertConfig = parseAlertConfig(config, options.alert);
248
+
247
249
  const mon = new Monitor({
248
250
  locations: options.locations,
249
251
  tags: options.tags,
250
- ...config,
252
+ ...normalizeConfig(config),
253
+ retestOnFailure,
251
254
  privateLocations,
252
255
  schedule: schedule || options.schedule,
253
256
  alert: alertConfig,
@@ -263,6 +266,14 @@ export function buildMonitorFromYaml(
263
266
  return mon;
264
267
  }
265
268
 
269
+ // Deletes unncessary fields from the lightweight monitor config
270
+ // that is not supported by the Kibana API
271
+ function normalizeConfig(config: MonitorConfig) {
272
+ delete config['private_locations'];
273
+ delete config['retest_on_failure'];
274
+ return config;
275
+ }
276
+
266
277
  export const parseAlertConfig = (
267
278
  config: MonitorConfig,
268
279
  gConfig?: AlertConfig
@@ -48,7 +48,8 @@ export async function sendRequest(options: APIRequestOptions) {
48
48
  'user-agent': `Elastic/Synthetics ${version}`,
49
49
  'kbn-xsrf': 'true',
50
50
  },
51
- headersTimeout: 60 * 1000,
51
+ // align with the default timeout of the kibana route
52
+ headersTimeout: 2 * 60 * 1000,
52
53
  });
53
54
  }
54
55
 
@@ -56,9 +57,16 @@ export async function sendReqAndHandleError<T>(
56
57
  options: APIRequestOptions
57
58
  ): Promise<T> {
58
59
  const { statusCode, body } = await sendRequest(options);
59
- return await (await handleError(statusCode, options.url, body)).json();
60
+ return (
61
+ await handleError(statusCode, options.url, body)
62
+ ).json() as Promise<T>;
60
63
  }
61
64
 
65
+ type APIError = {
66
+ error: string;
67
+ message: string;
68
+ };
69
+
62
70
  // Handle bad status code errors from Kibana API and format the
63
71
  // error message to be displayed to the user.
64
72
  // returns the response stream if no error is found
@@ -70,9 +78,9 @@ export async function handleError(
70
78
  if (statusCode === 404) {
71
79
  throw formatNotFoundError(url, await body.text());
72
80
  } else if (!ok(statusCode)) {
73
- let parsed: { error: string; message: string };
81
+ let parsed: APIError;
74
82
  try {
75
- parsed = await body.json();
83
+ parsed = (await body.json()) as APIError;
76
84
  } catch (e) {
77
85
  throw formatAPIError(
78
86
  statusCode,
package/src/push/utils.ts CHANGED
@@ -64,7 +64,7 @@ export function generateURL(options: PushOptions, operation: Operation) {
64
64
  const url = removeTrailingSlash(options.url);
65
65
  switch (operation) {
66
66
  case 'status':
67
- return `${url}/s/${options.space}/api/status`;
67
+ return `${url}/s/${options.space}/api/stats`;
68
68
  case 'bulk_get':
69
69
  case 'bulk_update':
70
70
  case 'bulk_delete':
@@ -24,14 +24,9 @@
24
24
  */
25
25
 
26
26
  import SonicBoom from 'sonic-boom';
27
- import { green, red, cyan } from 'kleur/colors';
28
- import {
29
- symbols,
30
- indent,
31
- now,
32
- findPWLogsIndexes,
33
- rewriteErrorStack,
34
- } from '../helpers';
27
+ import { green, red, cyan, gray } from 'kleur/colors';
28
+ import { renderError, serializeError } from './reporter-util';
29
+ import { symbols, indent, now } from '../helpers';
35
30
  import { Reporter, ReporterOptions } from '.';
36
31
  import {
37
32
  JourneyEndResult,
@@ -40,25 +35,6 @@ import {
40
35
  } from '../common_types';
41
36
  import { Journey, Step } from '../dsl';
42
37
 
43
- function renderError(error) {
44
- let output = '';
45
- const outer = indent('');
46
- const inner = indent(outer);
47
- const container = outer + '---\n';
48
- output += container;
49
- let stack = error.stack;
50
- if (stack) {
51
- output += inner + 'stack: |-\n';
52
- stack = rewriteErrorStack(stack, findPWLogsIndexes(stack));
53
- const lines = String(stack).split('\n');
54
- for (const line of lines) {
55
- output += inner + ' ' + line + '\n';
56
- }
57
- }
58
- output += container;
59
- return red(output);
60
- }
61
-
62
38
  function renderDuration(durationMs) {
63
39
  return parseInt(durationMs);
64
40
  }
@@ -66,14 +42,17 @@ function renderDuration(durationMs) {
66
42
  export default class BaseReporter implements Reporter {
67
43
  stream: SonicBoom;
68
44
  fd: number;
45
+ dryRun: boolean;
69
46
  metrics = {
70
47
  succeeded: 0,
71
48
  failed: 0,
72
49
  skipped: 0,
50
+ registered: 0,
73
51
  };
74
52
 
75
53
  constructor(options: ReporterOptions = {}) {
76
54
  this.fd = options.fd || process.stdout.fd;
55
+ this.dryRun = options.dryRun ?? false;
77
56
  /**
78
57
  * minLength is set to 1 byte to make sure we flush the
79
58
  * content even if its the last byte on the stream buffer
@@ -82,17 +61,25 @@ export default class BaseReporter implements Reporter {
82
61
  this.stream = new SonicBoom({ fd: this.fd, sync: true, minLength: 1 });
83
62
  }
84
63
 
64
+ onJourneyRegister(journey: Journey): void {
65
+ this.write(`\nJourney: ${journey.name}`);
66
+ this.metrics.registered++;
67
+ }
68
+
85
69
  onJourneyStart(journey: Journey, {}: JourneyStartResult) {
86
70
  this.write(`\nJourney: ${journey.name}`);
87
71
  }
88
72
 
89
73
  onStepEnd(_: Journey, step: Step, result: StepEndResult) {
90
74
  const { status, end, start, error } = result;
91
- const message = `${symbols[status]} Step: '${
92
- step.name
93
- }' ${status} (${renderDuration((end - start) * 1000)} ms)`;
75
+ const message = `${symbols[status]} Step: '${step.name}' ${status} ${gray(
76
+ '(' + renderDuration((end - start) * 1000) + ' ms)'
77
+ )}`;
94
78
  this.write(indent(message));
95
- error && this.write(renderError(error));
79
+ if (error) {
80
+ const message = renderError(serializeError(error));
81
+ this.write(indent(message) + '\n');
82
+ }
96
83
  this.metrics[status]++;
97
84
  }
98
85
 
@@ -105,14 +92,20 @@ export default class BaseReporter implements Reporter {
105
92
  * be executed which means the error happened in one of the hooks
106
93
  */
107
94
  if (total === 0 && error) {
108
- this.write(renderError(error));
95
+ const message = renderError(serializeError(error));
96
+ this.write(indent(message) + '\n');
109
97
  }
110
98
  }
111
99
 
112
100
  onEnd() {
113
- const { failed, succeeded, skipped } = this.metrics;
101
+ const { failed, succeeded, skipped, registered } = this.metrics;
114
102
  const total = failed + succeeded + skipped;
115
103
 
104
+ if (this.dryRun) {
105
+ this.write(`\n${registered} journey(s) registered`);
106
+ return;
107
+ }
108
+
116
109
  let message = '\n';
117
110
  if (total === 0) {
118
111
  message = 'No tests found!';
@@ -34,7 +34,11 @@ import {
34
34
  StepEndResult,
35
35
  } from '../common_types';
36
36
 
37
- export type ReporterOptions = { fd?: number; colors?: boolean };
37
+ export type ReporterOptions = {
38
+ fd?: number;
39
+ colors?: boolean;
40
+ dryRun?: boolean;
41
+ };
38
42
  export type BuiltInReporterName = 'default' | 'json' | 'junit';
39
43
  export type ReporterInstance = new (opts: ReporterOptions) => Reporter;
40
44
  export const reporters: {
@@ -28,8 +28,8 @@ import { join } from 'path';
28
28
  import sharp from 'sharp';
29
29
  import { createHash } from 'crypto';
30
30
  import BaseReporter from './base';
31
+ import { renderError, serializeError, stripAnsiCodes } from './reporter-util';
31
32
  import {
32
- formatError,
33
33
  getTimestamp,
34
34
  CACHE_PATH,
35
35
  totalist,
@@ -52,6 +52,7 @@ import {
52
52
  JourneyEndResult,
53
53
  PageMetrics,
54
54
  } from '../common_types';
55
+ import { inspect } from 'util';
55
56
 
56
57
  /* eslint-disable @typescript-eslint/no-var-requires */
57
58
  const { version, name } = require('../../package.json');
@@ -189,6 +190,41 @@ export function formatNetworkFields(network: NetworkInfo) {
189
190
  return { ecs, payload };
190
191
  }
191
192
 
193
+ /**
194
+ * formatJSONError formats the error in a structured format
195
+ * we restructure the error with code frame and stack trace for test errors
196
+ */
197
+ function formatJSONError(error: Error | any, type: OutputType) {
198
+ if (error == null) {
199
+ return;
200
+ }
201
+
202
+ // Early exit for non Error objects
203
+ if (!(error instanceof Error) || !error.stack) {
204
+ return { message: `thrown: ${inspect(error)}` };
205
+ }
206
+
207
+ /**
208
+ * Do not process browser errors - console.error and unhandled exceptions
209
+ */
210
+ if (type != null && type === 'journey/browserconsole') {
211
+ return {
212
+ name: error.name,
213
+ message: error.message,
214
+ stack: error.stack,
215
+ };
216
+ }
217
+
218
+ /**
219
+ * Do not highlight source for these errors and strip ANSI codes
220
+ */
221
+ return {
222
+ name: error.name,
223
+ message: stripAnsiCodes(error.message.split('\n')[0]),
224
+ stack: stripAnsiCodes(renderError(serializeError(error, false))),
225
+ };
226
+ }
227
+
192
228
  function journeyInfo(
193
229
  journey: OutputFields['journey'],
194
230
  type: OutputFields['type'],
@@ -317,7 +353,7 @@ export default class JSONReporter extends BaseReporter {
317
353
  });
318
354
  }
319
355
 
320
- onJourneyRegister(journey: Journey): void {
356
+ override onJourneyRegister(journey: Journey): void {
321
357
  this.writeJSON({
322
358
  type: 'journey/register',
323
359
  journey,
@@ -550,7 +586,7 @@ export default class JSONReporter extends BaseReporter {
550
586
  payload,
551
587
  blob,
552
588
  blob_mime,
553
- error: formatError(error),
589
+ error: formatJSONError(error, type),
554
590
  url,
555
591
  package_version: version,
556
592
  });
@@ -31,8 +31,9 @@ import {
31
31
  StepEndResult,
32
32
  } from '../common_types';
33
33
  import { Journey, Step } from '../dsl';
34
- import { formatError, indent, now } from '../helpers';
34
+ import { indent, now } from '../helpers';
35
35
  import BaseReporter from './base';
36
+ import { serializeError, stripAnsiCodes } from './reporter-util';
36
37
 
37
38
  type XMLEntry = {
38
39
  name: string;
@@ -97,14 +98,13 @@ export default class JUnitReporter extends BaseReporter {
97
98
 
98
99
  entry.attributes.tests++;
99
100
  if (status === 'failed') {
100
- const { name, message, stack } = formatError(error);
101
101
  caseEntry.children.push({
102
102
  name: 'failure',
103
103
  attributes: {
104
- message,
105
- type: name,
104
+ message: error.message,
105
+ type: error.name,
106
106
  },
107
- text: stack,
107
+ text: stripAnsiCodes(serializeError(error).stack),
108
108
  });
109
109
  entry.attributes.failures++;
110
110
  } else if (status === 'skipped') {