Skip to content

Commit c8ce614

Browse files
authored
Support error responses in network signals (#1164)
1 parent 29d1700 commit c8ce614

File tree

18 files changed

+806
-268
lines changed

18 files changed

+806
-268
lines changed

.changeset/blue-ads-doubt.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@segment/analytics-signals': minor
3+
---
4+
5+
Emit network signals that result in errors if the response has been emitted. Add ok and status to network signal data.

packages/signals/signals-example/src/components/ComplexForm.tsx

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,24 @@ import React, { FormEventHandler, useState } from 'react'
22

33
const ComplexForm = () => {
44
const [inputField, setInputField] = useState('')
5+
const [expectFormError, setExpectFormError] = useState(false)
56
const [selectField, setSelectField] = useState('')
67

8+
const statusCode: number = React.useMemo(() => {
9+
try {
10+
// Change the response status code via the text input field
11+
const val = parseInt(inputField, 10)
12+
return val >= 100 && val <= 511 ? val : 400
13+
} catch (err) {
14+
return 400
15+
}
16+
}, [inputField])
17+
718
const handleSubmit: FormEventHandler<HTMLFormElement> = (event) => {
819
event.preventDefault()
20+
921
const formData = {
22+
status: expectFormError ? statusCode : 200,
1023
inputField,
1124
selectField,
1225
}
@@ -33,16 +46,16 @@ const ComplexForm = () => {
3346
<button data-custom-attr="some-custom-attribute">Example Button</button>
3447
<form onSubmit={handleSubmit}>
3548
<div>
36-
<label htmlFor="ex-input">Input Field:</label>
49+
<label htmlFor="ex-input-text">Input some text:</label>
3750
<input
38-
id="ex-input"
51+
id="ex-input-text"
3952
type="text"
4053
value={inputField}
4154
onChange={(e) => setInputField(e.target.value)}
4255
/>
4356
</div>
4457
<div>
45-
<label htmlFor="ex-select">Select Field:</label>
58+
<label htmlFor="ex-select">Select an option:</label>
4659
<select
4760
id="ex-select"
4861
value={selectField}
@@ -55,7 +68,16 @@ const ComplexForm = () => {
5568
<option value="Option 2">Option 2</option>
5669
</select>
5770
</div>
58-
<button type="submit">Submit</button>
71+
<div>
72+
<label htmlFor="ex-checkbox">{`Force submit network status: ${statusCode}`}</label>
73+
<input
74+
id="ex-checkbox"
75+
type="checkbox"
76+
checked={expectFormError}
77+
onChange={(e) => setExpectFormError(e.target.checked)}
78+
/>
79+
</div>
80+
<button type="submit">Submit via network req</button>
5981
</form>
6082
<button>
6183
<div>

packages/signals/signals-example/src/styles.css

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,3 +40,7 @@ nav a {
4040
nav a:hover {
4141
color: #007bff;
4242
}
43+
44+
form div {
45+
display: inline-block;
46+
}

packages/signals/signals-example/webpack.config.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,8 @@ module.exports = {
5252
onBeforeSetupMiddleware(devServer) {
5353
devServer.app.use(bodyParser.json())
5454
devServer.app.post('/parrot', (req, res) => {
55-
console.log(req.body)
56-
res.json(req.body)
55+
console.log('/parrot', req.body)
56+
res.status(req.body.status ?? 200).json(req.body)
5757
})
5858
},
5959
historyApiFallback: true,

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ export class BasePage {
4949
await this.setupMockedRoutes()
5050
const url = options.updateURL ? options.updateURL(this.url) : this.url
5151
await this.page.goto(url)
52-
await this.invokeAnalyticsLoad(signalSettings)
52+
void this.invokeAnalyticsLoad(signalSettings)
5353
}
5454

5555
/**

packages/signals/signals-integration-tests/src/helpers/network-utils.ts

Lines changed: 42 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -18,63 +18,83 @@ export class PageNetworkUtils {
1818
async makeXHRCall(
1919
url = this.defaultTestApiURL,
2020
reqOptions: XHRRequestOptions = {}
21-
): Promise<void> {
21+
): Promise<any> {
2222
let normalizeUrl = url
2323
if (url.startsWith('/')) {
2424
normalizeUrl = new URL(url, this.page.url()).href
2525
}
2626
const req = this.page.waitForResponse(normalizeUrl ?? url, {
2727
timeout: this.defaultResponseTimeout,
2828
})
29-
await this.page.evaluate(
29+
const responseBody = this.page.evaluate(
3030
(args) => {
31-
const xhr = new XMLHttpRequest()
32-
xhr.open(args.method ?? 'POST', args.url)
33-
xhr.responseType = args.responseType ?? 'json'
34-
xhr.setRequestHeader(
35-
'Content-Type',
36-
args.contentType ?? 'application/json'
37-
)
38-
if (typeof args.responseLatency === 'number') {
39-
xhr.setRequestHeader(
40-
'x-test-latency',
41-
args.responseLatency.toString()
42-
)
43-
}
44-
xhr.send(args.body || JSON.stringify({ foo: 'bar' }))
31+
return new Promise<any>((resolve) => {
32+
const xhr = new XMLHttpRequest()
33+
xhr.open(args.method ?? 'POST', args.url)
34+
35+
const contentType = args.contentType ?? 'application/json'
36+
xhr.setRequestHeader('Content-Type', contentType)
37+
38+
xhr.responseType = args.responseType
39+
? args.responseType
40+
: contentType.includes('json')
41+
? 'json'
42+
: '' // '' is the same as 'text' according to xhr spec
43+
44+
if (typeof args.responseLatency === 'number') {
45+
xhr.setRequestHeader(
46+
'x-test-latency',
47+
args.responseLatency.toString()
48+
)
49+
}
50+
xhr.send(args.body ?? JSON.stringify({ foo: 'bar' }))
51+
xhr.onload = () => resolve(xhr.response)
52+
})
4553
},
4654
{ url, ...reqOptions }
4755
)
4856
await req
57+
return responseBody
4958
}
5059
/**
5160
* Make a fetch call in the page context. By default it will POST a JSON object with {foo: 'bar'}
5261
*/
5362
async makeFetchCall(
5463
url = this.defaultTestApiURL,
55-
request: Partial<RequestInit> = {}
56-
): Promise<void> {
64+
request: Partial<RequestInit> & {
65+
contentType?: string
66+
} = {}
67+
): Promise<any> {
5768
let normalizeUrl = url
5869
if (url.startsWith('/')) {
5970
normalizeUrl = new URL(url, this.page.url()).href
6071
}
6172
const req = this.page.waitForResponse(normalizeUrl ?? url, {
6273
timeout: this.defaultResponseTimeout,
6374
})
64-
await this.page.evaluate(
65-
(args) => {
66-
return fetch(args.url, {
75+
const responseBody = await this.page.evaluate(
76+
async (args) => {
77+
const res = await fetch(args.url, {
6778
method: 'POST',
6879
headers: {
69-
'Content-Type': 'application/json',
80+
'Content-Type': args.request.contentType ?? 'application/json',
7081
},
7182
body: JSON.stringify({ foo: 'bar' }),
7283
...args.request,
7384
})
85+
const type = res.headers.get('Content-Type')
86+
if (type?.includes('json')) {
87+
return res.json()
88+
} else if (type?.includes('text')) {
89+
return res.text()
90+
} else {
91+
console.error('Unexpected response content type')
92+
}
7493
},
7594
{ url, request }
7695
)
7796
await req
97+
return responseBody
7898
}
7999

80100
async mockTestRoute(

packages/signals/signals-integration-tests/src/tests/signals-vanilla/all-segment-events.test.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -48,11 +48,12 @@ test('Segment events', async ({ page }) => {
4848
}`
4949

5050
await indexPage.load(page, basicEdgeFn)
51-
await indexPage.clickButton()
52-
await Promise.all([
51+
const flush = Promise.all([
5352
indexPage.waitForSignalsApiFlush(),
5453
indexPage.waitForTrackingApiFlush(),
5554
])
55+
await indexPage.clickButton()
56+
await flush
5657

5758
const trackingApiReqs = indexPage.trackingAPI
5859
.getEvents()
@@ -74,16 +75,15 @@ test('Should dispatch events from signals that occurred before analytics was ins
7475
}`
7576

7677
await indexPage.load(page, edgeFn)
78+
const flush = Promise.all([
79+
indexPage.waitForSignalsApiFlush(),
80+
indexPage.waitForTrackingApiFlush(),
81+
])
7782

7883
// add a user defined signal before analytics is instantiated
7984
void indexPage.addUserDefinedSignal()
85+
await flush
8086

81-
await indexPage.waitForSettings()
82-
83-
await Promise.all([
84-
indexPage.waitForSignalsApiFlush(),
85-
indexPage.waitForTrackingApiFlush(),
86-
])
8787
const trackingApiReqs = indexPage.trackingAPI.getEvents()
8888
expect(trackingApiReqs).toHaveLength(2)
8989

packages/signals/signals-integration-tests/src/tests/signals-vanilla/basic.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ test.beforeEach(async ({ page }) => {
1616
await indexPage.loadAndWait(page, basicEdgeFn)
1717
})
1818

19-
test('network signals', async () => {
19+
test('network signals fetch', async () => {
2020
/**
2121
* Make a fetch call, see if it gets sent to the signals endpoint
2222
*/

0 commit comments

Comments
 (0)