Skip to content

Commit 9fc8f43

Browse files
authored
improve Segment.io retries (#714)
1 parent 779e66b commit 9fc8f43

File tree

15 files changed

+120
-29
lines changed

15 files changed

+120
-29
lines changed

.changeset/clean-lizards-nail.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@segment/analytics-next': patch
3+
---
4+
5+
Improves Segment.io retries to include exponential backoff and work across page loads.

packages/browser-integration-tests/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
"@playwright/test": "^1.28.1",
2121
"@segment/analytics-next": "workspace:^",
2222
"http-server": "14.1.1",
23-
"nock": "^13.2.9"
23+
"nock": "^13.2.9",
24+
"tslib": "^2.4.1"
2425
}
2526
}

packages/browser-integration-tests/src/segment-retries.test.ts

Lines changed: 56 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { test, expect } from '@playwright/test'
1+
import { Request, test, expect } from '@playwright/test'
22
import { SettingsBuilder } from './fixtures/settings'
33
import { standaloneMock } from './helpers/standalone-mock'
44

@@ -25,7 +25,9 @@ test.describe('Standalone tests', () => {
2525
)
2626
})
2727

28-
test.skip('supports retries on page navigation', async ({ page }) => {
28+
test('supports retrying failed requests on page navigation', async ({
29+
page,
30+
}) => {
2931
// Load analytics.js
3032
await page.goto('/standalone.html')
3133
await page.evaluate(() => window.analytics.load('fake-key'))
@@ -40,16 +42,16 @@ test.describe('Standalone tests', () => {
4042
times: 1,
4143
}
4244
)
43-
const requestFailure = new Promise((resolve) => {
44-
page.once('requestfailed', resolve)
45+
const requestFailure = new Promise<Record<string, any>>((resolve) => {
46+
page.once('requestfailed', (request) => resolve(request.postDataJSON()))
4547
})
4648

4749
// trigger an event
4850
await page.evaluate(() => {
4951
void window.analytics.track('test event')
5052
})
5153

52-
await requestFailure
54+
const { messageId } = await requestFailure
5355
await page.reload()
5456

5557
// load analytics.js again and wait for a new request.
@@ -59,6 +61,55 @@ test.describe('Standalone tests', () => {
5961
])
6062

6163
expect(request.method()).toBe('POST')
64+
expect(request.postDataJSON().messageId).toBe(messageId)
65+
})
66+
67+
test('supports retrying in-flight requests on page navigation', async ({
68+
page,
69+
}) => {
70+
// Load analytics.js
71+
await page.goto('/standalone.html')
72+
await page.evaluate(() => window.analytics.load('fake-key'))
73+
74+
// blackhole the request so that it stays in-flight when we reload the page
75+
await page.route(
76+
'https://api.segment.io/v1/t',
77+
async () => {
78+
// do nothing
79+
},
80+
{
81+
times: 1,
82+
}
83+
)
84+
85+
// Detect when we've seen a track request initiated by the browser
86+
const requestSent = new Promise<Record<string, any>>((resolve) => {
87+
const onRequest: (req: Request) => void = (req) => {
88+
if (req.url() === 'https://api.segment.io/v1/t') {
89+
page.off('request', onRequest)
90+
resolve(req.postDataJSON())
91+
}
92+
}
93+
94+
page.on('request', onRequest)
95+
})
96+
97+
// trigger an event
98+
await page.evaluate(() => {
99+
void window.analytics.track('test event')
100+
})
101+
102+
const { messageId } = await requestSent
103+
await page.reload()
104+
105+
// load analytics.js again and wait for a new request.
106+
const [request] = await Promise.all([
107+
page.waitForRequest('https://api.segment.io/v1/t'),
108+
page.evaluate(() => window.analytics.load('fake-key')),
109+
])
110+
111+
expect(request.method()).toBe('POST')
112+
expect(request.postDataJSON().messageId).toBe(messageId)
62113
})
63114
})
64115
})
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import type { AnalyticsSnippet } from '@segment/analytics-next'
2+
3+
declare global {
4+
interface Window {
5+
analytics: AnalyticsSnippet
6+
}
7+
}

packages/browser/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@
5656
"js-cookie": "3.0.1",
5757
"node-fetch": "^2.6.7",
5858
"spark-md5": "^3.0.1",
59-
"tslib": "^2.4.0",
59+
"tslib": "^2.4.1",
6060
"unfetch": "^4.1.0"
6161
},
6262
"devDependencies": {

packages/browser/src/lib/__tests__/on-page-leave.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@ import { onPageLeave } from '../on-page-leave'
22

33
const helpers = {
44
dispatchEvent(event: 'pagehide' | 'visibilitychange') {
5-
document.dispatchEvent(new Event(event))
5+
const target = event === 'pagehide' ? window : document
6+
target.dispatchEvent(new Event(event))
67
},
78
setVisibilityState(state: DocumentVisibilityState) {
89
Object.defineProperty(document, 'visibilityState', {

packages/browser/src/lib/on-page-leave.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,13 @@
1010
export const onPageLeave = (cb: (...args: unknown[]) => void) => {
1111
let unloaded = false // prevents double firing if both are supported
1212

13-
// using document instead of window because of bug affecting browsers before safari 14 (detail in footnotes https://caniuse.com/?search=visibilitychange)
14-
document.addEventListener('pagehide', () => {
13+
window.addEventListener('pagehide', () => {
1514
if (unloaded) return
1615
unloaded = true
1716
cb()
1817
})
1918

19+
// using document instead of window because of bug affecting browsers before safari 14 (detail in footnotes https://caniuse.com/?search=visibilitychange)
2020
document.addEventListener('visibilitychange', () => {
2121
if (document.visibilityState == 'hidden') {
2222
if (unloaded) return

packages/browser/src/lib/priority-queue/__tests__/persisted.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ describe('Persisted Priority Queue', () => {
4141
let onUnload: any = jest.fn()
4242

4343
jest
44-
.spyOn(document, 'addEventListener')
44+
.spyOn(window, 'addEventListener')
4545
.mockImplementation((evt, handler) => {
4646
if (evt === 'pagehide') {
4747
onUnload = handler
@@ -102,7 +102,7 @@ describe('Persisted Priority Queue', () => {
102102
let onUnload: any = jest.fn()
103103

104104
jest
105-
.spyOn(document, 'addEventListener')
105+
.spyOn(window, 'addEventListener')
106106
.mockImplementation((evt, handler) => {
107107
if (evt === 'pagehide') {
108108
onUnload = handler
@@ -200,7 +200,7 @@ describe('Persisted Priority Queue', () => {
200200
const onUnloadFunctions: any[] = []
201201

202202
jest
203-
.spyOn(document, 'addEventListener')
203+
.spyOn(window, 'addEventListener')
204204
.mockImplementation((evt, handler) => {
205205
if (evt === 'pagehide') {
206206
onUnloadFunctions.push(handler)

packages/browser/src/lib/priority-queue/persisted.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ export class PersistedPriorityQueue extends PriorityQueue<Context> {
109109
}
110110
})
111111

112-
document.addEventListener('pagehide', () => {
112+
window.addEventListener('pagehide', () => {
113113
// we deliberately want to use the less powerful 'pagehide' API to only persist on events where the analytics instance gets destroyed, and not on tab away.
114114
if (this.todo > 0) {
115115
const items = [...this.queue, ...this.future]

packages/browser/src/plugins/segmentio/__tests__/batched-dispatcher.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -224,7 +224,7 @@ describe('Batching', () => {
224224

225225
expect(fetch).not.toHaveBeenCalled()
226226

227-
document.dispatchEvent(new Event('pagehide'))
227+
window.dispatchEvent(new Event('pagehide'))
228228

229229
expect(fetch).toHaveBeenCalledTimes(1)
230230

@@ -253,7 +253,7 @@ describe('Batching', () => {
253253

254254
expect(fetch).not.toHaveBeenCalled()
255255

256-
document.dispatchEvent(new Event('pagehide'))
256+
window.dispatchEvent(new Event('pagehide'))
257257

258258
expect(fetch).toHaveBeenCalledTimes(2)
259259
})

0 commit comments

Comments
 (0)