Skip to content

Commit c13e3d7

Browse files
feat/browser performance scrapping, batch service for requests
1 parent 6d0d15b commit c13e3d7

14 files changed

Lines changed: 415 additions & 54 deletions

File tree

packages/browser/global.d.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { BrowserClientConfigType } from "./src/types/client";
2+
3+
declare global {
4+
var __TRACEO__: BrowserClientConfigType
5+
}
6+
7+
export { };

packages/browser/src/client.ts

Lines changed: 49 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
import { IBrowserClient, TraceoBrowserError, TraceoOptions } from "./types/client";
1+
import { BrowserClientConfigType, IBrowserClient, TraceoBrowserError } from "./types/client";
22
import { Transport } from "./transport";
3+
import { Performance } from "./performance";
34
import { utils } from "./utils";
45
import { stacktrace } from "./exceptions/stacktrace";
56
import { Trace } from "./types/stacktrace";
@@ -8,19 +9,41 @@ import { EventOnErrorType, EventOnUnhandledRejectionType, windowEventHandlers }
89
import { BrowserInfoType } from "./types/browser";
910

1011
export abstract class BrowserClient implements IBrowserClient {
11-
public headers!: { [key: string]: any };
12-
public options: TraceoOptions;
13-
public transport: Transport;
12+
private configs: BrowserClientConfigType;
13+
14+
private transport: Transport;
15+
private performance: Performance;
1416
private browser: BrowserInfoType;
1517

16-
constructor(options: TraceoOptions) {
17-
this.options = options;
18-
this.transport = new Transport(this.options);
18+
constructor(options: BrowserClientConfigType) {
19+
this.configs = options;
20+
21+
this.configGlobalClient();
22+
23+
this.transport = new Transport(this.configs);
24+
this.performance = new Performance(this.configs);
25+
1926
this.browser = utils.browserDetails();
2027

2128
this.initSDK();
2229
}
2330

31+
private initSDK(): void {
32+
if (this.isOffline) {
33+
return;
34+
}
35+
36+
if (window !== undefined) {
37+
this.initWindowEventHandlers();
38+
}
39+
40+
if (this.configs.options.performance) {
41+
this.performance.initPerformanceCollection();
42+
}
43+
44+
this.postInitSDK();
45+
}
46+
2447
/**
2548
* Method to implement dedicated logic only for a specific type of SDK.
2649
* Implement in dedicated client.
@@ -35,7 +58,7 @@ export abstract class BrowserClient implements IBrowserClient {
3558
public handleError(error: TraceoBrowserError): void {
3659
const err = this.constructError(error);
3760
if (err) {
38-
this.transport.send<BrowserIncidentType>(err, this.headers);
61+
this.transport.send<BrowserIncidentType>(this.incidentsUrl, err, this.configs.headers);
3962
}
4063
}
4164

@@ -44,12 +67,11 @@ export abstract class BrowserClient implements IBrowserClient {
4467
* @param error
4568
*/
4669
private sendError(error: BrowserIncidentType): void {
47-
this.transport.send<BrowserIncidentType>(error, this.headers);
70+
this.transport.send<BrowserIncidentType>(this.incidentsUrl, error, this.configs.headers);
4871
}
4972

5073
private constructError(error?: Error): BrowserIncidentType | null {
5174
if (!error) {
52-
console.debug("-- : --");
5375
return null;
5476
}
5577

@@ -71,16 +93,19 @@ export abstract class BrowserClient implements IBrowserClient {
7193
return err;
7294
}
7395

74-
private initSDK(): void {
75-
if (this.isOffline) {
76-
return;
77-
}
78-
79-
if (window !== undefined) {
80-
this.initWindowEventHandlers();
81-
}
96+
/**
97+
* Persist configs data (client options and headers) inside browser window object
98+
*/
99+
private configGlobalClient(): void {
100+
window.__TRACEO__ = this.configs;
101+
}
82102

83-
this.postInitSDK();
103+
/**
104+
* Public accessor to browser client configs
105+
* @see also utils.getGlobalConfigs
106+
*/
107+
public static get client(): BrowserClientConfigType {
108+
return window.__TRACEO__;
84109
}
85110

86111
private initWindowEventHandlers() {
@@ -98,7 +123,6 @@ export abstract class BrowserClient implements IBrowserClient {
98123
if (data.error) {
99124
const eventError = this.constructError(data?.error);
100125
if (!eventError) {
101-
console.debug("-- --- --");
102126
return;
103127
}
104128

@@ -139,6 +163,10 @@ export abstract class BrowserClient implements IBrowserClient {
139163
};
140164

141165
private get isOffline(): boolean | undefined {
142-
return this.options.offline;
166+
return this.configs.options.offline;
167+
}
168+
169+
private get incidentsUrl() {
170+
return `/api/worker/incident/${this.configs.options.appId}`;
143171
}
144172
}
Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
import { Batch } from "../transport/batch";
2+
import { BrowserClientConfigType } from "../types/client";
3+
import { BrowserPerformanceType, LargestContentfulPaint, LayoutShift, ObserverType } from "../types/performance";
4+
5+
export class Performance {
6+
private batch: Batch;
7+
8+
constructor(configs: BrowserClientConfigType) {
9+
this.batch = new Batch(configs, {
10+
headers: configs?.headers,
11+
url: `/api/worker/browser/perf/${configs?.options?.appId}`
12+
});
13+
}
14+
15+
public initPerformanceCollection() {
16+
this.handleCLS();
17+
this.handleFID();
18+
this.handleLCP();
19+
this.handleNavigationEntry();
20+
this.handlePaintEntry();
21+
}
22+
23+
// https://developer.mozilla.org/en-US/docs/Web/API/PerformancePaintTiming
24+
// first-paint, first-contentful-paint
25+
private handlePaintEntry() {
26+
const handle = (entries: PerformancePaintTiming[]): void => {
27+
for (const entry of entries) {
28+
const entryName = entry.name === "first-paint"
29+
? "FP"
30+
: entry.name === "first-contentful-paint"
31+
? "FCP"
32+
: undefined;
33+
34+
const payload: BrowserPerformanceType = {
35+
event: entry.entryType,
36+
performance: [{
37+
name: entryName,
38+
unit: "miliseconds",
39+
value: entry.startTime
40+
}]
41+
};
42+
43+
this.batch.add(payload);
44+
}
45+
}
46+
47+
this.observe<LargestContentfulPaint>("paint", handle);
48+
}
49+
50+
// largest-contentful-paint
51+
// https://developer.mozilla.org/en-US/docs/Web/API/LargestContentfulPaint
52+
private handleLCP() {
53+
const handle = (entries: LargestContentfulPaint[]): void => {
54+
if (entries.length === 0) {
55+
return;
56+
}
57+
58+
const entry = entries[entries.length - 1];
59+
const payload: BrowserPerformanceType = {
60+
event: entry.entryType,
61+
performance: [{
62+
value: entry.startTime,
63+
unit: 'millisecond',
64+
name: "LCP"
65+
}]
66+
};
67+
this.batch.add(payload);
68+
}
69+
this.observe<LargestContentfulPaint>("largest-contentful-paint", handle);
70+
}
71+
72+
// layout-shift
73+
// https://developer.mozilla.org/en-US/docs/Web/API/LayoutShift
74+
private handleCLS() {
75+
const handle = (entries: LayoutShift[]): void => {
76+
for (const entry of entries) {
77+
if (entry.hadRecentInput) {
78+
// layout-shift event is triggered after every user input
79+
return;
80+
}
81+
82+
const payload: BrowserPerformanceType = {
83+
event: entry.entryType,
84+
performance: [{
85+
name: "CLS",
86+
unit: " ",
87+
value: entry.value
88+
}]
89+
}
90+
this.batch.add(payload);
91+
}
92+
93+
}
94+
this.observe<LayoutShift>("layout-shift", handle);
95+
}
96+
97+
// first-input-delay
98+
// https://developer.mozilla.org/en-US/docs/Glossary/First_input_delay
99+
private handleFID() {
100+
const handle = (entries: PerformanceEventTiming[]): void => {
101+
for (const entry of entries) {
102+
const payload: BrowserPerformanceType = {
103+
event: entry.entryType,
104+
performance: [{
105+
name: "FID",
106+
unit: "miliseconds",
107+
value: entry.processingStart - entry.startTime
108+
}]
109+
};
110+
this.batch.add(payload);
111+
}
112+
}
113+
this.observe<PerformanceEventTiming>("first-input", handle);
114+
}
115+
116+
// https://developer.mozilla.org/en-US/docs/Web/API/PerformanceNavigationTiming
117+
private handleNavigationEntry() {
118+
const handle = (entries: PerformanceNavigationTiming[]): void => {
119+
for (const entry of entries) {
120+
const payload: BrowserPerformanceType = {
121+
event: entry.entryType,
122+
performance: [
123+
{
124+
name: "domCompleted",
125+
unit: "miliseconds",
126+
value: entry.domComplete
127+
},
128+
{
129+
name: "domContentLoadedEventEnd",
130+
unit: "miliseconds",
131+
value: entry.domContentLoadedEventEnd
132+
},
133+
{
134+
name: "domInteractive",
135+
unit: "miliseconds",
136+
value: entry.domInteractive
137+
},
138+
{
139+
name: "loadEventEnd",
140+
unit: "miliseconds",
141+
value: entry.loadEventEnd
142+
}
143+
]
144+
};
145+
146+
this.batch.add(payload);
147+
}
148+
};
149+
150+
this.observe<PerformanceNavigationTiming>("navigation", handle);
151+
}
152+
153+
private observe = <T>(
154+
type: ObserverType,
155+
callback: (entries: T[]) => void
156+
): PerformanceObserver | undefined => {
157+
if (window.PerformanceObserver) {
158+
const supported = PerformanceObserver.supportedEntryTypes;
159+
if (!supported.includes(type)) {
160+
return;
161+
}
162+
163+
const observe = new PerformanceObserver((entries => callback(entries.getEntries() as T[])));
164+
observe.observe({ type, buffered: true });
165+
166+
return observe;
167+
};
168+
169+
return undefined;
170+
}
171+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { Transport } from ".";
2+
import { BrowserClientConfigType, Dictionary } from "../types/client";
3+
import { BatchPayload, BatchOptions } from "../types/transport";
4+
import { utils } from "../utils";
5+
6+
const DEFAULT_MESSAGES_BYTES_LIMIT = 524288; //0.5MB
7+
const DEFAULT_MESSAGES_COUNT = 50;
8+
const DEFAULT_BATCH_TIMEOUT = 60; //60 seconds
9+
10+
export class Batch {
11+
private headers: Dictionary<string>;
12+
private url: string;
13+
14+
private batchMessage: BatchPayload[] = [];
15+
private batchMessageCount: number = 0;
16+
private batchMessageBytes: number = 0;
17+
18+
private batchMaxMessageCount: number = 0;
19+
private batchMaxMessageBytes: number = 0;
20+
21+
private transport: Transport;
22+
23+
constructor(clientOptions: BrowserClientConfigType, batchOptions: BatchOptions) {
24+
this.transport = new Transport(clientOptions);
25+
26+
this.headers = batchOptions.headers;
27+
this.url = batchOptions.url;
28+
29+
this.batchMaxMessageCount = batchOptions.batchMaxMessageCount || DEFAULT_MESSAGES_COUNT;
30+
this.batchMaxMessageBytes = batchOptions.batchMaxMessageBytes || DEFAULT_MESSAGES_BYTES_LIMIT;
31+
32+
window.addEventListener("beforeunload", () => this.flush());
33+
this.flushOnInterval();
34+
}
35+
36+
private flushOnInterval() {
37+
setInterval(() => {
38+
this.flush()
39+
}, DEFAULT_BATCH_TIMEOUT * 1000);
40+
}
41+
42+
public add(payload: BatchPayload) {
43+
this.batchMessageCount += 1;
44+
this.batchMessage.push(payload);
45+
this.batchMessageBytes += utils.toBytes(payload);
46+
47+
if (this.shouldFlush) {
48+
this.flush();
49+
}
50+
}
51+
52+
private get shouldFlush(): boolean {
53+
return (
54+
this.batchMessageCount >= this.batchMaxMessageCount ||
55+
this.batchMessageBytes >= this.batchMaxMessageBytes
56+
);
57+
}
58+
59+
private flush() {
60+
const message = this.batchMessage;
61+
this.batchMessageCount = 0;
62+
this.batchMessageBytes = 0;
63+
this.batchMessage = [];
64+
65+
this.transport.send(this.url, message, this.headers);
66+
}
67+
}

0 commit comments

Comments
 (0)