Skip to content

Commit 87b2447

Browse files
committed
feat(plugin): allow to transform entries before saving a HAR file
closes #174
1 parent 1cb3186 commit 87b2447

11 files changed

+206
-52
lines changed

src/Plugin.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { RecordOptions, SaveOptions } from './Plugin';
22
import { Plugin } from './Plugin';
3-
import { Logger } from './utils/Logger';
4-
import { FileManager } from './utils/FileManager';
3+
import type { Logger } from './utils/Logger';
4+
import type { FileManager } from './utils/FileManager';
55
import type {
66
Observer,
77
ObserverFactory,

src/Plugin.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ export interface SaveOptions {
2828
export type RecordOptions = NetworkObserverOptions & {
2929
rootDir: string;
3030
filter?: string;
31+
transform?: string;
3132
};
3233

3334
interface Addr {
@@ -81,10 +82,7 @@ export class Plugin {
8182
);
8283
}
8384

84-
this.exporter = await this.exporterFactory.create({
85-
predicatePath: options.filter,
86-
rootDir: options.rootDir
87-
});
85+
this.exporter = await this.exporterFactory.create(options);
8886
this._connection = this.connectionFactory.create({
8987
...this.addr,
9088
maxRetries: 20,

src/cdp/DefaultNetwork.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { Network, NetworkEvent } from '../network';
22
import { ErrorUtils } from '../utils/ErrorUtils';
3-
import { Logger } from '../utils/Logger';
3+
import type { Logger } from '../utils/Logger';
44
import {
55
TARGET_OR_BROWSER_CLOSED,
66
UNABLE_TO_ATTACH_TO_TARGET

src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ const plugin = new Plugin(
2222
FileManager.Instance,
2323
new DefaultConnectionFactory(Logger.Instance),
2424
new DefaultObserverFactory(Logger.Instance),
25-
new DefaultHarExporterFactory(FileManager.Instance)
25+
new DefaultHarExporterFactory(FileManager.Instance, Logger.Instance)
2626
);
2727

2828
export const install = (on: Cypress.PluginEvents): void => {
Lines changed: 71 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
import { NetworkRequest } from './NetworkRequest';
22
import { DefaultHarExporter } from './DefaultHarExporter';
3+
import type { Logger } from '../utils/Logger';
4+
import type { DefaultHarExporterOptions } from './DefaultHarExporterOptions';
35
import {
46
anyString,
57
instance,
68
match,
79
mock,
810
reset,
11+
spy,
912
verify,
1013
when
1114
} from 'ts-mockito';
@@ -15,31 +18,47 @@ import type { WriteStream } from 'fs';
1518
import { EOL } from 'os';
1619

1720
describe('DefaultHarExporter', () => {
18-
const buffer = mock<WriteStream>();
21+
const streamMock = mock<WriteStream>();
22+
const loggerMock = mock<Logger>();
1923
const networkRequest = new NetworkRequest(
2024
'1',
2125
'https://example.com',
2226
'https://example.com',
2327
'1'
2428
);
25-
const predicate: jest.Mock<(entry: Entry) => Promise<unknown> | unknown> =
26-
jest.fn<(entry: Entry) => Promise<unknown> | unknown>();
29+
const predicate = jest.fn<(entry: Entry) => Promise<unknown> | unknown>();
30+
const transform = jest.fn<(entry: Entry) => Promise<Entry> | Entry>();
31+
2732
let harExporter!: DefaultHarExporter;
33+
let options!: DefaultHarExporterOptions;
34+
let optionsSpy!: DefaultHarExporterOptions;
2835

2936
beforeEach(() => {
30-
harExporter = new DefaultHarExporter(instance(buffer), predicate);
37+
options = {};
38+
optionsSpy = spy(options);
39+
40+
harExporter = new DefaultHarExporter(
41+
instance(loggerMock),
42+
instance(streamMock),
43+
options
44+
);
3145
});
3246

3347
afterEach(() => {
3448
predicate.mockRestore();
35-
reset(buffer);
49+
transform.mockRestore();
50+
reset<DefaultHarExporterOptions | WriteStream | Logger>(
51+
streamMock,
52+
optionsSpy,
53+
loggerMock
54+
);
3655
});
3756

3857
describe('path', () => {
3958
it('should return the path serializing buffer', () => {
4059
// arrange
4160
const expected = '/path/file';
42-
when(buffer.path).thenReturn(Buffer.from(expected));
61+
when(streamMock.path).thenReturn(Buffer.from(expected));
4362

4463
// act
4564
const result = harExporter.path;
@@ -51,7 +70,7 @@ describe('DefaultHarExporter', () => {
5170
it('should return the path', () => {
5271
// arrange
5372
const expected = '/path/file';
54-
when(buffer.path).thenReturn(expected);
73+
when(streamMock.path).thenReturn(expected);
5574

5675
// act
5776
const result = harExporter.path;
@@ -67,28 +86,30 @@ describe('DefaultHarExporter', () => {
6786
harExporter.end();
6887

6988
// assert
70-
verify(buffer.end()).once();
89+
verify(streamMock.end()).once();
7190
});
7291
});
7392

7493
describe('write', () => {
7594
it('should write the entry to the buffer', async () => {
7695
// arrange
7796
// @ts-expect-error type mismatch
78-
when(buffer.closed).thenReturn(false);
97+
when(streamMock.closed).thenReturn(false);
98+
when(optionsSpy.predicate).thenReturn(predicate);
7999
predicate.mockReturnValue(false);
80100

81101
// act
82102
await harExporter.write(networkRequest);
83103

84104
// assert
85-
verify(buffer.write(match(`${EOL}`))).once();
105+
verify(streamMock.write(match(`${EOL}`))).once();
86106
});
87107

88108
it('should write the entry to the buffer if the predicate returns throws an error', async () => {
89109
// arrange
90110
// @ts-expect-error type mismatch
91-
when(buffer.closed).thenReturn(false);
111+
when(streamMock.closed).thenReturn(false);
112+
when(optionsSpy.predicate).thenReturn(predicate);
92113
predicate.mockReturnValue(
93114
Promise.reject(new Error('something went wrong'))
94115
);
@@ -97,33 +118,67 @@ describe('DefaultHarExporter', () => {
97118
await harExporter.write(networkRequest);
98119

99120
// assert
100-
verify(buffer.write(match(`${EOL}`))).once();
121+
verify(streamMock.write(match(`${EOL}`))).once();
122+
});
123+
124+
it('should transform the entry before writing to the buffer', async () => {
125+
// arrange
126+
const entry = { foo: 'bar' } as unknown as Entry;
127+
const entryString = JSON.stringify(entry);
128+
// @ts-expect-error type mismatch
129+
when(streamMock.closed).thenReturn(false);
130+
when(optionsSpy.transform).thenReturn(transform);
131+
transform.mockReturnValue(Promise.resolve(entry));
132+
133+
// act
134+
await harExporter.write(networkRequest);
135+
136+
// assert
137+
verify(streamMock.write(match(`${entryString}${EOL}`))).once();
138+
});
139+
140+
it('should skip the entry when the transformation is failed with an error', async () => {
141+
// arrange
142+
// @ts-expect-error type mismatch
143+
when(streamMock.closed).thenReturn(false);
144+
when(optionsSpy.transform).thenReturn(transform);
145+
transform.mockReturnValue(
146+
Promise.reject(new Error('Something went wrong.'))
147+
);
148+
149+
// act
150+
await harExporter.write(networkRequest);
151+
152+
// assert
153+
verify(streamMock.write(anyString())).never();
101154
});
102155

103156
it('should not write the entry to the buffer if the predicate returns true', async () => {
104157
// arrange
105158
// @ts-expect-error type mismatch
106-
when(buffer.closed).thenReturn(false);
159+
when(streamMock.closed).thenReturn(false);
160+
when(optionsSpy.predicate).thenReturn(predicate);
107161
predicate.mockReturnValue(true);
108162

109163
// act
110164
await harExporter.write(networkRequest);
111165

112166
// assert
113-
verify(buffer.write(anyString())).never();
167+
verify(streamMock.write(anyString())).never();
114168
});
115169

116170
it('should not write the entry to the buffer if the buffer is closed', async () => {
117171
// arrange
118172
// @ts-expect-error type mismatch
119-
when(buffer.closed).thenReturn(true);
173+
when(streamMock.closed).thenReturn(true);
174+
when(optionsSpy.predicate).thenReturn(predicate);
120175
predicate.mockReturnValue(false);
121176

122177
// act
123178
await harExporter.write(networkRequest);
124179

125180
// assert
126-
verify(buffer.write(anyString())).never();
181+
verify(streamMock.write(anyString())).never();
127182
});
128183
});
129184
});

src/network/DefaultHarExporter.ts

Lines changed: 54 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,17 @@
11
import { EntryBuilder } from './EntryBuilder';
22
import type { NetworkRequest } from './NetworkRequest';
33
import type { HarExporter } from './HarExporter';
4+
import type { Logger } from '../utils/Logger';
5+
import { ErrorUtils } from '../utils/ErrorUtils';
6+
import type {
7+
DefaultHarExporterOptions,
8+
Predicate,
9+
Transformer
10+
} from './DefaultHarExporterOptions';
411
import type { Entry } from 'har-format';
512
import type { WriteStream } from 'fs';
613
import { EOL } from 'os';
14+
import { format } from 'util';
715

816
export class DefaultHarExporter implements HarExporter {
917
get path(): string {
@@ -12,9 +20,18 @@ export class DefaultHarExporter implements HarExporter {
1220
return Buffer.isBuffer(path) ? path.toString('utf-8') : path;
1321
}
1422

23+
private get predicate(): Predicate | undefined {
24+
return this.options?.predicate;
25+
}
26+
27+
private get transform(): Transformer | undefined {
28+
return this.options?.transform;
29+
}
30+
1531
constructor(
32+
private readonly logger: Logger,
1633
private readonly buffer: WriteStream,
17-
private readonly predicate?: (entry: Entry) => Promise<unknown> | unknown
34+
private readonly options?: DefaultHarExporterOptions
1835
) {}
1936

2037
public async write(networkRequest: NetworkRequest): Promise<void> {
@@ -24,24 +41,56 @@ export class DefaultHarExporter implements HarExporter {
2441
return;
2542
}
2643

27-
const json = JSON.stringify(entry);
44+
const json = await this.serializeEntry(entry);
2845

2946
// @ts-expect-error type mismatch
30-
if (!this.buffer.closed) {
47+
if (!this.buffer.closed && json) {
3148
this.buffer.write(`${json}${EOL}`);
3249
}
3350
}
3451

52+
public async serializeEntry(entry: Entry): Promise<string | undefined> {
53+
try {
54+
const result =
55+
typeof this.transform === 'function'
56+
? await this.transform(entry)
57+
: entry;
58+
59+
return JSON.stringify(result);
60+
} catch (e) {
61+
const stack = ErrorUtils.isError(e) ? e.stack : e;
62+
const formattedEntry = format('%j', entry);
63+
64+
this.logger.err(
65+
`The entry is missing as a result of an error in the 'transform' function.
66+
67+
The passed entry:
68+
${formattedEntry}
69+
70+
The stack trace for this error is:
71+
${stack}`
72+
);
73+
74+
return undefined;
75+
}
76+
}
77+
3578
public end(): void {
3679
this.buffer.end();
3780
}
3881

39-
private async applyPredicate(entry: Entry) {
82+
private async applyPredicate(entry: Entry): Promise<unknown> {
4083
try {
4184
return (
4285
typeof this.predicate === 'function' && (await this.predicate?.(entry))
4386
);
44-
} catch {
87+
} catch (e) {
88+
const message = ErrorUtils.isError(e) ? e.message : e;
89+
90+
this.logger.debug(
91+
`The operation has encountered an error while processing the entry. ${message}`
92+
);
93+
4594
return false;
4695
}
4796
}

0 commit comments

Comments
 (0)