Skip to content

Commit 8d1e715

Browse files
authored
feat: getResourceType() uses initiatorType when file extension missing (#451)
* feat: use initiatorType when getResourceFileType() fails * chore: check if initiator type exists
1 parent c0aa33a commit 8d1e715

File tree

5 files changed

+142
-33
lines changed

5 files changed

+142
-33
lines changed

src/plugins/event-plugins/ResourcePlugin.ts

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,5 @@
11
import { InternalPlugin } from '../InternalPlugin';
2-
import {
3-
getResourceFileType,
4-
ResourceType,
5-
shuffle
6-
} from '../../utils/common-utils';
2+
import { getResourceFileType, shuffle } from '../../utils/common-utils';
73
import { ResourceEvent } from '../../events/resource-event';
84
import { PERFORMANCE_RESOURCE_EVENT_TYPE } from '../utils/constant';
95
import {
@@ -63,7 +59,9 @@ export class ResourcePlugin extends InternalPlugin {
6359
list.filter((e) => e.entryType === RESOURCE)
6460
.filter((e) => !this.config.ignore(e))
6561
.forEach((event) => {
66-
const type: ResourceType = getResourceFileType(event.name);
62+
const { name, initiatorType } =
63+
event as PerformanceResourceTiming;
64+
const type = getResourceFileType(name, initiatorType);
6765
if (this.config.recordAllTypes.includes(type)) {
6866
recordAll.push(event);
6967
} else if (this.config.sampleTypes.includes(type)) {
@@ -84,10 +82,15 @@ export class ResourcePlugin extends InternalPlugin {
8482
}
8583
};
8684

87-
recordResourceEvent = (entryData: PerformanceResourceTiming): void => {
85+
recordResourceEvent = ({
86+
name,
87+
initiatorType,
88+
duration,
89+
transferSize
90+
}: PerformanceResourceTiming): void => {
8891
const pathRegex =
8992
/.*\/application\/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\/events/;
90-
const entryUrl = new URL(entryData.name);
93+
const entryUrl = new URL(name);
9194
if (
9295
entryUrl.host === this.context.config.endpointUrl.host &&
9396
pathRegex.test(entryUrl.pathname)
@@ -101,13 +104,13 @@ export class ResourcePlugin extends InternalPlugin {
101104
if (this.context?.record) {
102105
const eventData: ResourceEvent = {
103106
version: '1.0.0',
104-
initiatorType: entryData.initiatorType,
105-
duration: entryData.duration,
106-
fileType: getResourceFileType(entryData.name),
107-
transferSize: entryData.transferSize
107+
initiatorType,
108+
duration,
109+
fileType: getResourceFileType(name, initiatorType),
110+
transferSize
108111
};
109112
if (this.context.config.recordResourceUrl) {
110-
eventData.targetUrl = entryData.name;
113+
eventData.targetUrl = name;
111114
}
112115
this.context.record(PERFORMANCE_RESOURCE_EVENT_TYPE, eventData);
113116
}

src/plugins/event-plugins/__tests__/ResourcePlugin.test.ts

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import {
2-
resourceEvent,
2+
resourceTiming,
33
putRumEventsDocument,
44
putRumEventsGammaDocument,
55
dataPlaneDocument,
@@ -27,7 +27,7 @@ const buildResourcePlugin = (config?: PartialPerformancePluginConfig) => {
2727

2828
describe('ResourcePlugin tests', () => {
2929
beforeEach(() => {
30-
doMockPerformanceObserver([navigationEvent, resourceEvent]);
30+
doMockPerformanceObserver([navigationEvent, resourceTiming]);
3131
record.mockClear();
3232
});
3333

@@ -50,11 +50,12 @@ describe('ResourcePlugin tests', () => {
5050
);
5151
expect(record.mock.calls[0][1]).toEqual(
5252
expect.objectContaining({
53-
fileType: resourceEvent.fileType,
54-
duration: resourceEvent.duration,
55-
transferSize: resourceEvent.transferSize,
56-
targetUrl: resourceEvent.name,
57-
initiatorType: resourceEvent.initiatorType
53+
version: '1.0.0',
54+
fileType: 'script',
55+
duration: resourceTiming.duration,
56+
transferSize: resourceTiming.transferSize,
57+
targetUrl: resourceTiming.name,
58+
initiatorType: resourceTiming.initiatorType
5859
})
5960
);
6061
});

src/test-utils/mock-data.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ export const navigationEventNotLoaded = {
8585
navigationTimingLevel: 2
8686
};
8787

88-
export const resourceEvent = {
88+
export const resourceTiming: PerformanceResourceTiming = {
8989
connectEnd: 0,
9090
connectStart: 0,
9191
decodedBodySize: 0,
@@ -108,7 +108,7 @@ export const resourceEvent = {
108108
startTime: 357.59500000131084,
109109
transferSize: 0,
110110
workerStart: 0,
111-
fileType: 'other'
111+
toJSON: function () {} // eslint-disable-line
112112
};
113113

114114
export const resourceEvent2 = {
@@ -335,7 +335,7 @@ export class MockPerformanceObserver {
335335
}
336336

337337
observe(options: ObserveInterface): void {
338-
this.cb({ getEntries: () => [navigationEvent, resourceEvent] });
338+
this.cb({ getEntries: () => [navigationEvent, resourceTiming] });
339339
}
340340

341341
disconnect(): void {

src/utils/__tests__/common-utils.test.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,4 +60,49 @@ describe('Common utils tests', () => {
6060
utils.ResourceType.FONT
6161
);
6262
});
63+
64+
test('when resource is image but file extension is no match, then initiatorType resolves to image', async () => {
65+
// Init
66+
const resourceUrl = 'example.com';
67+
// Assert
68+
expect(
69+
utils.getResourceFileType(resourceUrl, utils.InitiatorType.IMG)
70+
).toEqual(utils.ResourceType.IMAGE);
71+
expect(
72+
utils.getResourceFileType(resourceUrl, utils.InitiatorType.IMAGE)
73+
).toEqual(utils.ResourceType.IMAGE);
74+
expect(
75+
utils.getResourceFileType(resourceUrl, utils.InitiatorType.INPUT)
76+
).toEqual(utils.ResourceType.IMAGE);
77+
});
78+
79+
test('when resource is document but file extension is no match, then initiatorType resolves to document', async () => {
80+
// Init
81+
const resourceUrl = 'example.com';
82+
// Assert
83+
expect(
84+
utils.getResourceFileType(resourceUrl, utils.InitiatorType.IFRAME)
85+
).toEqual(utils.ResourceType.DOCUMENT);
86+
expect(
87+
utils.getResourceFileType(resourceUrl, utils.InitiatorType.FRAME)
88+
).toEqual(utils.ResourceType.DOCUMENT);
89+
});
90+
91+
test('when resource is script but file extension is no match, then initiatorType resolves to script', async () => {
92+
// Init
93+
const resourceUrl = 'example.com';
94+
// Assert
95+
expect(
96+
utils.getResourceFileType(resourceUrl, utils.InitiatorType.SCRIPT)
97+
).toEqual(utils.ResourceType.SCRIPT);
98+
});
99+
100+
test('when resource is stylesheet but file extension is no match, then initiatorType resolves to stylesheet', async () => {
101+
// Init
102+
const resourceUrl = 'example.com';
103+
// Assert
104+
expect(
105+
utils.getResourceFileType(resourceUrl, utils.InitiatorType.CSS)
106+
).toEqual(utils.ResourceType.STYLESHEET);
107+
});
63108
});

src/utils/common-utils.ts

Lines changed: 70 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,36 @@ export enum ResourceType {
77
FONT = 'font'
88
}
99

10+
/**
11+
* https://developer.mozilla.org/en-US/docs/Web/API/PerformanceResourceTiming/initiatorType
12+
*/
13+
export enum InitiatorType {
14+
/**
15+
* IMAGES
16+
* PerformanceResourceTiming with initiatorType=Input must be an image
17+
* Per MDN docs: "if the request was initiated by an <input> element of type image.""
18+
*/
19+
IMG = 'img',
20+
IMAGE = 'image',
21+
INPUT = 'input',
22+
23+
/**
24+
* DOCUMENTS
25+
*/
26+
IFRAME = 'iframe',
27+
FRAME = 'frame',
28+
29+
/**
30+
* SCRIPTS
31+
*/
32+
SCRIPT = 'script',
33+
34+
/**
35+
* STYLESHEETS
36+
*/
37+
CSS = 'css'
38+
}
39+
1040
const extensions = [
1141
{
1242
name: ResourceType.STYLESHEET,
@@ -52,18 +82,48 @@ export const shuffle = (a: any[]) => {
5282
}
5383
};
5484

55-
export const getResourceFileType = (url: string): ResourceType => {
56-
const filename = url.substring(url.lastIndexOf('/') + 1);
57-
const extension = filename
58-
.substring(filename.lastIndexOf('.') + 1)
59-
.split(/[?#]/)[0];
60-
85+
export const getResourceFileType = (
86+
url: string,
87+
initiatorType?: string
88+
): ResourceType => {
6189
let ext = ResourceType.OTHER;
62-
extensions.forEach((type) => {
63-
if (type.list.indexOf(extension) > -1) {
64-
ext = type.name;
90+
if (url) {
91+
const filename = url.substring(url.lastIndexOf('/') + 1);
92+
const extension = filename
93+
.substring(filename.lastIndexOf('.') + 1)
94+
.split(/[?#]/)[0];
95+
96+
extensions.forEach((type) => {
97+
if (type.list.indexOf(extension) > -1) {
98+
ext = type.name;
99+
}
100+
});
101+
}
102+
103+
/**
104+
* Resource name sometimes does not have the correct file extension names due to redirects.
105+
* In these cases, they are mislablled as "other". In these cases, we can infer the correct
106+
* fileType from the initiator.
107+
*/
108+
if (initiatorType && ext === ResourceType.OTHER) {
109+
switch (initiatorType) {
110+
case InitiatorType.IMAGE:
111+
case InitiatorType.IMG:
112+
case InitiatorType.INPUT:
113+
ext = ResourceType.IMAGE;
114+
break;
115+
case InitiatorType.IFRAME:
116+
case InitiatorType.FRAME:
117+
ext = ResourceType.DOCUMENT;
118+
break;
119+
case InitiatorType.SCRIPT:
120+
ext = ResourceType.SCRIPT;
121+
break;
122+
case InitiatorType.CSS:
123+
ext = ResourceType.STYLESHEET;
124+
break;
65125
}
66-
});
126+
}
67127
return ext;
68128
};
69129

0 commit comments

Comments
 (0)