Skip to content

Commit 11a943e

Browse files
authored
Fix runtime errors on signals, allow plain/text in response (#1177)
1 parent 7d5d975 commit 11a943e

File tree

21 files changed

+271
-102
lines changed

21 files changed

+271
-102
lines changed

.changeset/little-chefs-hunt.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
'@segment/analytics-signals': minor
3+
---
4+
- Fix runtime errors for submit
5+
- Add better form submit data
6+
- Loosen content-type to parse text/plain
7+
- Tweak disallow list
8+
- Add labels

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

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,13 +57,38 @@ export class PageNetworkUtils {
5757
await req
5858
return responseBody
5959
}
60+
async makeFileUploadRequest(url: string) {
61+
let normalizeUrl = url
62+
if (url.startsWith('/')) {
63+
normalizeUrl = new URL(url, this.page.url()).href
64+
}
65+
const req = this.page.waitForResponse(normalizeUrl ?? url, {
66+
timeout: this.defaultResponseTimeout,
67+
})
68+
await this.page.evaluate((_url) => {
69+
const formData = new FormData()
70+
const file = new File(['file content'], 'test.txt', {
71+
type: 'text/plain',
72+
})
73+
formData.append('file', file)
74+
return fetch(_url, {
75+
method: 'POST',
76+
body: formData,
77+
headers: {
78+
'Content-Type': 'multipart/form-data',
79+
},
80+
})
81+
}, normalizeUrl)
82+
await req
83+
}
6084
/**
6185
* Make a fetch call in the page context. By default it will POST a JSON object with {foo: 'bar'}
6286
*/
6387
async makeFetchCall(
6488
url = this.defaultTestApiURL,
6589
request: Partial<RequestInit> & {
6690
contentType?: string
91+
blob?: boolean
6792
} = {}
6893
): Promise<any> {
6994
let normalizeUrl = url

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,10 +164,11 @@ test('navigation signals', async ({ page }) => {
164164

165165
// navigate to a new hash
166166
{
167+
const flush = indexPage.waitForSignalsApiFlush()
167168
await page.evaluate(() => {
168169
window.location.hash = '#foo'
169170
})
170-
await indexPage.waitForSignalsApiFlush()
171+
await flush
171172
expect(indexPage.signalsAPI.getEvents()).toHaveLength(2)
172173
const ev = indexPage.signalsAPI.lastEvent('navigation')
173174
expect(ev.properties).toMatchObject({

packages/signals/signals-integration-tests/src/tests/signals-vanilla/change-input.test.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,10 @@ test('Collecting signals whenever a user enters text input', async ({
3939
labels: [
4040
{
4141
textContent: 'Name:',
42+
id: '',
43+
attributes: {
44+
for: 'name',
45+
},
4246
},
4347
],
4448
name: 'name',

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

Lines changed: 64 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,60 @@ test.describe('network signals - fetch', () => {
1010
indexPage = await new IndexPage().loadAndWait(page, basicEdgeFn)
1111
})
1212

13-
test('should not emit anything if neither request nor response are json', async () => {
13+
test('should not emit non-json requests', async () => {
14+
await indexPage.network.mockTestRoute('http://localhost/upload', {
15+
body: JSON.stringify({ foo: 'test' }),
16+
})
17+
18+
await indexPage.network.makeFileUploadRequest('http://localhost/upload')
19+
20+
await indexPage.waitForSignalsApiFlush()
21+
22+
const networkEvents = indexPage.signalsAPI.getEvents('network')
23+
24+
// Check the request
25+
const requests = networkEvents.filter(
26+
(el) => el.properties!.data.action === 'request'
27+
)
28+
29+
expect(requests).toHaveLength(0)
30+
})
31+
32+
test('should try to parse the body of text/plain requests', async () => {
1433
await indexPage.network.mockTestRoute('http://localhost/test', {
15-
body: 'hello',
34+
body: JSON.stringify({ foo: 'test' }),
35+
contentType: 'application/json',
36+
})
37+
38+
await indexPage.network.makeFetchCall('http://localhost/test', {
39+
method: 'POST',
40+
body: JSON.stringify({ key: 'value' }),
1641
contentType: 'text/plain',
1742
})
1843

44+
await indexPage.waitForSignalsApiFlush()
45+
46+
const networkEvents = indexPage.signalsAPI.getEvents('network')
47+
48+
// Check the request
49+
const requests = networkEvents.filter(
50+
(el) => el.properties!.data.action === 'request'
51+
)
52+
53+
expect(requests).toHaveLength(1)
54+
expect(requests[0].properties!.data).toMatchObject({
55+
action: 'request',
56+
url: 'http://localhost/test',
57+
data: { key: 'value' },
58+
})
59+
})
60+
61+
test('should send the raw string if the request body cannot be parsed as json', async () => {
62+
await indexPage.network.mockTestRoute('http://localhost/test', {
63+
body: JSON.stringify({ foo: 'test' }),
64+
contentType: 'application/json',
65+
})
66+
1967
await indexPage.network.makeFetchCall('http://localhost/test', {
2068
method: 'POST',
2169
body: 'hello world',
@@ -26,7 +74,17 @@ test.describe('network signals - fetch', () => {
2674

2775
const networkEvents = indexPage.signalsAPI.getEvents('network')
2876

29-
expect(networkEvents).toHaveLength(0)
77+
// Check the request
78+
const requests = networkEvents.filter(
79+
(el) => el.properties!.data.action === 'request'
80+
)
81+
82+
expect(requests).toHaveLength(1)
83+
expect(requests[0].properties!.data).toMatchObject({
84+
action: 'request',
85+
url: 'http://localhost/test',
86+
data: 'hello world',
87+
})
3088
})
3189

3290
test('can make a basic json request / not break regular fetch calls', async () => {
@@ -56,6 +114,7 @@ test.describe('network signals - fetch', () => {
56114
expect(requests).toHaveLength(1)
57115
expect(requests[0].properties!.data).toEqual({
58116
action: 'request',
117+
contentType: 'application/json',
59118
url: 'http://localhost/test',
60119
method: 'POST',
61120
data: { key: 'value' },
@@ -67,6 +126,7 @@ test.describe('network signals - fetch', () => {
67126
expect(responses).toHaveLength(1)
68127
expect(responses[0].properties!.data).toEqual({
69128
action: 'response',
129+
contentType: 'application/json',
70130
url: 'http://localhost/test',
71131
data: { foo: 'test' },
72132
status: 200,
@@ -112,39 +172,6 @@ test.describe('network signals - fetch', () => {
112172
})
113173
})
114174

115-
test('should emit response but not request if request content-type is not json but response is', async () => {
116-
await indexPage.network.mockTestRoute('http://localhost/test', {
117-
body: JSON.stringify({ foo: 'test' }),
118-
contentType: 'application/json',
119-
})
120-
121-
await indexPage.network.makeFetchCall('http://localhost/test', {
122-
method: 'POST',
123-
body: 'hello world',
124-
contentType: 'text/plain',
125-
})
126-
127-
await indexPage.waitForSignalsApiFlush()
128-
129-
const networkEvents = indexPage.signalsAPI.getEvents('network')
130-
131-
const responses = networkEvents.filter(
132-
(el) => el.properties!.data.action === 'response'
133-
)
134-
expect(responses).toHaveLength(1)
135-
expect(responses[0].properties!.data).toMatchObject({
136-
action: 'response',
137-
url: 'http://localhost/test',
138-
data: { foo: 'test' },
139-
})
140-
141-
// Ensure no request was captured
142-
const requests = networkEvents.filter(
143-
(el) => el.properties!.data.action === 'request'
144-
)
145-
expect(requests).toHaveLength(0)
146-
})
147-
148175
test.describe('errors', () => {
149176
test('will handle a json error response', async () => {
150177
await indexPage.network.mockTestRoute('http://localhost/test', {
@@ -232,11 +259,7 @@ test.describe('network signals - fetch', () => {
232259
status: 400,
233260
})
234261

235-
await indexPage.network.makeFetchCall('http://localhost/test', {
236-
method: 'POST',
237-
body: 'hello world',
238-
contentType: 'text/plain',
239-
})
262+
await indexPage.network.makeFileUploadRequest('http://localhost/test')
240263

241264
await indexPage.waitForSignalsApiFlush()
242265

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

Lines changed: 13 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { test, expect } from '@playwright/test'
22
import { IndexPage } from './index-page'
3-
import { sleep } from '@segment/analytics-core'
43

54
const basicEdgeFn = `const processSignal = (signal) => {}`
65

@@ -10,27 +9,6 @@ test.describe('network signals - XHR', () => {
109
test.beforeEach(async ({ page }) => {
1110
indexPage = await new IndexPage().loadAndWait(page, basicEdgeFn)
1211
})
13-
test('should not emit anything if neither request nor response are json', async () => {
14-
await indexPage.network.mockTestRoute('http://localhost/test', {
15-
body: 'hello',
16-
contentType: 'text/plain',
17-
})
18-
19-
await indexPage.network.makeXHRCall('http://localhost/test', {
20-
method: 'POST',
21-
body: 'hello world',
22-
contentType: 'text/plain',
23-
responseType: 'text',
24-
})
25-
26-
// Wait for the signals to be flushed
27-
await sleep(300)
28-
29-
const networkEvents = indexPage.signalsAPI.getEvents('network')
30-
31-
// Ensure no request or response was captured
32-
expect(networkEvents).toHaveLength(0)
33-
})
3412

3513
test('basic json request / not break XHR', async () => {
3614
await indexPage.network.mockTestRoute('http://localhost/test', {
@@ -111,7 +89,7 @@ test.describe('network signals - XHR', () => {
11189
})
11290
})
11391

114-
test('should emit response but not request if request content-type is not json but response is', async () => {
92+
test('should emit request content type, even if not json', async () => {
11593
await indexPage.network.mockTestRoute('http://localhost/test', {
11694
body: JSON.stringify({ foo: 'test' }),
11795
contentType: 'application/json',
@@ -128,7 +106,18 @@ test.describe('network signals - XHR', () => {
128106

129107
const networkEvents = await indexPage.signalsAPI.waitForEvents(1, 'network')
130108

131-
// Check the response (only response should be captured)
109+
// ensure request
110+
const requests = networkEvents.filter(
111+
(el) => el.properties!.data.action === 'request'
112+
)
113+
expect(requests).toHaveLength(1)
114+
expect(requests[0].properties!.data).toMatchObject({
115+
action: 'request',
116+
url: 'http://localhost/test',
117+
data: 'hello world',
118+
})
119+
120+
// Check the response
132121
const responses = networkEvents.filter(
133122
(el) => el.properties!.data.action === 'response'
134123
)
@@ -138,12 +127,6 @@ test.describe('network signals - XHR', () => {
138127
url: 'http://localhost/test',
139128
data: { foo: 'test' },
140129
})
141-
142-
// Ensure no request was captured
143-
const requests = networkEvents.filter(
144-
(el) => el.properties!.data.action === 'request'
145-
)
146-
expect(requests).toHaveLength(0)
147130
})
148131

149132
test('should parse response if responseType is set to json but response header does not contain application/json', async () => {

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

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,11 @@ const basicEdgeFn = `const processSignal = (signal) => {}`
88
test('ingestion not enabled -> will not send the signal', async ({ page }) => {
99
await indexPage.loadAndWait(page, basicEdgeFn, {
1010
enableSignalsIngestion: false,
11+
flushAt: 1,
1112
})
12-
1313
await indexPage.fillNameInput('John Doe')
14-
await indexPage.waitForSignalsApiFlush().catch(() => {
15-
expect(true).toBe(true)
16-
})
14+
await page.waitForTimeout(100)
15+
expect(indexPage.signalsAPI.getEvents('interaction')).toHaveLength(0)
1716
})
1817

1918
test('ingestion enabled -> will send the signal', async ({ page }) => {
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
# @segment/analytics-signals-runtime
2-
Encapsults Signals runtime functionality, in order to share logic between the signals plugins for browser and mobile.
2+
Encapsulates Signals runtime functionality, in order to share logic between the signals plugins for browser and mobile.
33

44
### Development
55
`yarn build` generate the following artifacts:
66
| Generated File(s) | Description |
77
|--------|-------------|
88
| `/dist/runtime/index.[platform].js`, `/[platform]/get-runtime-code.generated.js` | Exposes `globalThis.Signals` and constants (e.g. `SignalType`), either through the script tag or in the mobile JS engine |
99
| `/dist/editor/[platform]-editor.d.ts.txt` | Type definitions for monaco editor on app.segment.com
10-
| `/dist/esm/index.js` | Entry point for `@segment/analytics-signals` and the segment app, that want to consume the types / runtime code. |
10+
| `/dist/esm/index.js` | Entry point for `@segment/analytics-signals` and the segment app, for consumers that want to consume the types / runtime code as an npm package. |

packages/signals/signals-runtime/src/test-helpers/mocks/mock-signal-types-web.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ export const mockNetworkSignal: NetworkSignal = {
3838
type: 'network',
3939
data: {
4040
action: 'request',
41+
contentType: 'application/json',
4142
url: 'https://api.example.com/data',
4243
method: 'GET',
4344
data: { key: 'value' },

packages/signals/signals-runtime/src/web/web-signals-types.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ type ClickData = {
2121

2222
type SubmitData = {
2323
eventType: 'submit'
24-
submitter: SerializedTarget
24+
submitter?: SerializedTarget
25+
target: SerializedTarget
2526
}
2627

2728
type ChangeData = {
@@ -68,6 +69,7 @@ interface BaseNetworkData {
6869
action: string
6970
url: string
7071
data: JSONValue
72+
contentType: string
7173
}
7274

7375
interface NetworkRequestData extends BaseNetworkData {

0 commit comments

Comments
 (0)