Skip to content

Commit 101e841

Browse files
authored
XHR Network signals URL normalization (#1152)
1 parent 571386f commit 101e841

File tree

7 files changed

+166
-10
lines changed

7 files changed

+166
-10
lines changed

.changeset/odd-pans-drop.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
'@segment/analytics-signals': minor
3+
---
4+
- normalize XHR URL, http methods, etc

packages/signals/signals-integration-tests/src/helpers/base-page-object.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ export class BasePage {
2222
this.url = 'http://localhost:5432/src/tests' + path
2323
}
2424

25+
public origin() {
26+
return new URL(this.page.url()).origin
27+
}
28+
2529
/**
2630
* Load and setup routes
2731
* and wait for analytics and signals to be initialized
@@ -196,6 +200,9 @@ export class BasePage {
196200
url = BasePage.defaultTestApiURL,
197201
response?: Partial<FulfillOptions>
198202
) {
203+
if (url.startsWith('/')) {
204+
url = new URL(url, this.page.url()).href
205+
}
199206
await this.page.route(url, (route) => {
200207
return route.fulfill({
201208
contentType: 'application/json',
@@ -210,7 +217,11 @@ export class BasePage {
210217
url = BasePage.defaultTestApiURL,
211218
request?: Partial<RequestInit>
212219
): Promise<void> {
213-
const req = this.page.waitForRequest(url)
220+
let normalizeUrl = url
221+
if (url.startsWith('/')) {
222+
normalizeUrl = new URL(url, this.page.url()).href
223+
}
224+
const req = this.page.waitForResponse(normalizeUrl ?? url)
214225
await this.page.evaluate(
215226
({ url, request }) => {
216227
return fetch(url, {
@@ -238,7 +249,11 @@ export class BasePage {
238249
responseType: XMLHttpRequestResponseType
239250
}> = {}
240251
): Promise<void> {
241-
const req = this.page.waitForRequest(url)
252+
let normalizeUrl = url
253+
if (url.startsWith('/')) {
254+
normalizeUrl = new URL(url, this.page.url()).href
255+
}
256+
const req = this.page.waitForResponse(normalizeUrl ?? url)
242257
await this.page.evaluate(
243258
({ url, body, contentType, method, responseType }) => {
244259
const xhr = new XMLHttpRequest()

packages/signals/signals-integration-tests/src/tests/signals-vanilla/network-signals-xhr.test.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,55 @@ test.describe('XHR Tests', () => {
9393
})
9494
})
9595

96+
test('works with XHR and relative paths', async () => {
97+
await indexPage.mockTestRoute(`/test`, {
98+
body: JSON.stringify({ foo: 'test' }),
99+
contentType: 'application/json',
100+
})
101+
102+
await indexPage.makeXHRCall('/test', {
103+
method: 'POST',
104+
body: JSON.stringify({ key: 'value' }),
105+
responseType: 'json',
106+
contentType: 'application/json',
107+
})
108+
109+
// Wait for the signals to be flushed
110+
await indexPage.waitForSignalsApiFlush()
111+
112+
// Retrieve the batch of events from the signals request
113+
const batch = indexPage.lastSignalsApiReq.postDataJSON()
114+
.batch as SegmentEvent[]
115+
116+
// Filter out network events
117+
const networkEvents = batch.filter(
118+
(el) => el.properties!.type === 'network'
119+
)
120+
121+
// Check the request
122+
const requests = networkEvents.filter(
123+
(el) => el.properties!.data.action === 'request'
124+
)
125+
126+
expect(requests).toHaveLength(1)
127+
expect(requests[0].properties!.data).toMatchObject({
128+
action: 'request',
129+
url: `${indexPage.origin()}/test`,
130+
data: { key: 'value' },
131+
})
132+
133+
// Check the response
134+
const responses = networkEvents.filter(
135+
(el) => el.properties!.data.action === 'response'
136+
)
137+
expect(responses).toHaveLength(1)
138+
expect(responses[0].properties!.data).toMatchObject({
139+
action: 'response',
140+
url: `${indexPage.origin()}/test`,
141+
data: { foo: 'test' },
142+
})
143+
})
144+
96145
test('should emit response but not request if request content-type is not json but response is', async () => {
97146
await indexPage.mockTestRoute('http://localhost/test', {
98147
body: JSON.stringify({ foo: 'test' }),

packages/signals/signals/src/core/signal-generators/network-gen/__tests__/network-generator.test.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,7 @@ describe(NetworkGenerator, () => {
149149
"data": {
150150
"data": "test",
151151
},
152-
"url": "/api",
152+
"url": "http://localhost/api",
153153
},
154154
"metadata": {
155155
"filters": {
@@ -245,6 +245,25 @@ describe(NetworkGenerator, () => {
245245
unregister()
246246
})
247247

248+
it('should default to GET method if no method is provided', async () => {
249+
const mockEmitter = { emit: jest.fn() }
250+
const networkGenerator = new TestNetworkGenerator()
251+
const unregister = networkGenerator.register(
252+
mockEmitter as unknown as SignalEmitter
253+
)
254+
255+
await window.fetch(`/test`, {
256+
headers: { 'content-type': 'application/json' },
257+
body: JSON.stringify({ key: 'value' }),
258+
})
259+
260+
await sleep(100)
261+
const [first] = mockEmitter.emit.mock.calls
262+
263+
expect(first[0].data.method).toBe('GET')
264+
unregister()
265+
})
266+
248267
it('emits signals for same domain if networkSignalsAllowSameDomain = false', async () => {
249268
const mockEmitter = { emit: jest.fn() }
250269
const networkGenerator = new TestNetworkGenerator({

packages/signals/signals/src/core/signal-generators/network-gen/index.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import {
66
import { createNetworkSignal } from '../../../types'
77
import { SignalEmitter } from '../../emitter'
88
import { SignalsSettingsConfig } from '../../signals'
9-
import { normalizeUrl } from '../../../lib/normalize-url'
109
import { SignalGenerator } from '../types'
1110
import {
1211
NetworkInterceptor,
@@ -70,8 +69,8 @@ export class NetworkGenerator implements SignalGenerator {
7069
createNetworkSignal(
7170
{
7271
action: 'request',
73-
url: normalizeUrl(sUrl),
74-
method: rq.method || '',
72+
url: sUrl,
73+
method: rq.method || 'GET',
7574
data: JSON.parse(rq.body.toString()),
7675
},
7776
createMetadata()
@@ -95,7 +94,7 @@ export class NetworkGenerator implements SignalGenerator {
9594
createNetworkSignal(
9695
{
9796
action: 'response',
98-
url: url,
97+
url,
9998
data: data,
10099
},
101100
createMetadata()
@@ -124,7 +123,7 @@ export class NetworkGenerator implements SignalGenerator {
124123
createNetworkSignal(
125124
{
126125
action: 'request',
127-
url: normalizeUrl(sUrl),
126+
url: sUrl,
128127
method: rq.method,
129128
data: tryParseXHRBody(rq.body),
130129
},
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import {
2+
createNetworkSignal,
3+
NetworkData,
4+
NetworkSignalMetadata,
5+
} from '../signals'
6+
import { normalizeUrl } from '../../lib/normalize-url'
7+
8+
jest.mock('../../lib/normalize-url', () => ({
9+
normalizeUrl: jest.fn((url) => url),
10+
}))
11+
12+
describe(createNetworkSignal, () => {
13+
const metadata: NetworkSignalMetadata = {
14+
filters: {
15+
allowed: ['allowed1', 'allowed2'],
16+
disallowed: ['disallowed1', 'disallowed2'],
17+
},
18+
}
19+
20+
it('should create a network signal for a request', () => {
21+
const data: NetworkData = {
22+
action: 'request',
23+
url: 'http://example.com',
24+
method: 'post',
25+
data: { key: 'value' },
26+
}
27+
28+
const signal = createNetworkSignal(data, metadata)
29+
30+
expect(signal).toEqual({
31+
type: 'network',
32+
data: {
33+
action: 'request',
34+
url: 'http://example.com',
35+
method: 'POST',
36+
data: { key: 'value' },
37+
},
38+
metadata,
39+
})
40+
expect(normalizeUrl).toHaveBeenCalledWith('http://example.com')
41+
})
42+
43+
it('should create a network signal for a response', () => {
44+
const data: NetworkData = {
45+
action: 'response',
46+
url: 'http://example.com',
47+
data: { key: 'value' },
48+
}
49+
50+
const signal = createNetworkSignal(data, metadata)
51+
52+
expect(signal).toEqual({
53+
type: 'network',
54+
data: {
55+
action: 'response',
56+
url: 'http://example.com',
57+
data: { key: 'value' },
58+
},
59+
metadata,
60+
})
61+
expect(normalizeUrl).toHaveBeenCalledWith('http://example.com')
62+
})
63+
})

packages/signals/signals/src/types/signals.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { JSONValue } from '@segment/analytics-next'
2+
import { normalizeUrl } from '../lib/normalize-url'
23

34
export type SignalType =
45
| 'navigation'
@@ -65,7 +66,7 @@ export type InstrumentationSignal = AppSignal<
6566
InstrumentationData
6667
>
6768

68-
interface NetworkSignalMetadata {
69+
export interface NetworkSignalMetadata {
6970
filters: {
7071
allowed: string[]
7172
disallowed: string[]
@@ -171,7 +172,13 @@ export const createNetworkSignal = (
171172
): NetworkSignal => {
172173
return {
173174
type: 'network',
174-
data,
175+
data: {
176+
...data,
177+
url: normalizeUrl(data.url),
178+
...(data.action === 'request'
179+
? { method: data.method.toUpperCase() }
180+
: {}),
181+
},
175182
metadata: metadata,
176183
}
177184
}

0 commit comments

Comments
 (0)