Skip to content

Commit 0b4e5b8

Browse files
authored
Add PendingEventsDispatcher in event-processor package (#251)
1 parent 6d1a320 commit 0b4e5b8

13 files changed

+699
-68
lines changed

packages/event-processor/CHANGELOG.MD

+5
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
77
## [Unreleased]
88
Changes that have landed but are not yet released.
99

10+
## [0.2.0] - March 27, 2019
11+
12+
- Add `PendingEventsDispatcher` to wrap another EventDispatcher with retry support for
13+
events that did not send successfully due to page navigation
14+
1015
## [0.1.0] - March 1, 2019
1116

1217
Initial release
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
1+
/**
2+
* Copyright 2019, Optimizely
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
/// <reference types="jest" />
17+
18+
jest.mock('@optimizely/js-sdk-utils', () => ({
19+
__esModule: true,
20+
generateUUID: jest.fn(),
21+
getTimestamp: jest.fn(),
22+
objectValues: jest.requireActual('@optimizely/js-sdk-utils').objectValues,
23+
}))
24+
25+
import {
26+
LocalStoragePendingEventsDispatcher,
27+
PendingEventsDispatcher,
28+
DispatcherEntry,
29+
} from '../src/pendingEventsDispatcher'
30+
import { EventDispatcher, EventV1Request } from '../src/eventDispatcher'
31+
import { EventV1 } from '../src/v1/buildEventV1'
32+
import { PendingEventsStore, LocalStorageStore } from '../src/pendingEventsStore'
33+
import { generateUUID, getTimestamp } from '@optimizely/js-sdk-utils'
34+
35+
describe('LocalStoragePendingEventsDispatcher', () => {
36+
let originalEventDispatcher: EventDispatcher
37+
let pendingEventsDispatcher: PendingEventsDispatcher
38+
39+
beforeEach(() => {
40+
originalEventDispatcher = {
41+
dispatchEvent: jest.fn(),
42+
}
43+
pendingEventsDispatcher = new LocalStoragePendingEventsDispatcher({
44+
eventDispatcher: originalEventDispatcher,
45+
})
46+
;((getTimestamp as unknown) as jest.Mock).mockReturnValue(1)
47+
;((generateUUID as unknown) as jest.Mock).mockReturnValue('uuid')
48+
})
49+
50+
afterEach(() => {
51+
localStorage.clear()
52+
})
53+
54+
it('should properly send the events to the passed in eventDispatcher, when callback statusCode=200', () => {
55+
const callback = jest.fn()
56+
const eventV1Request: EventV1Request = {
57+
url: 'http://cdn.com',
58+
httpVerb: 'POST',
59+
params: ({ id: 'event' } as unknown) as EventV1,
60+
}
61+
62+
pendingEventsDispatcher.dispatchEvent(eventV1Request, callback)
63+
64+
expect(callback).not.toHaveBeenCalled()
65+
// manually invoke original eventDispatcher callback
66+
const internalDispatchCall = ((originalEventDispatcher.dispatchEvent as unknown) as jest.Mock)
67+
.mock.calls[0]
68+
internalDispatchCall[1]({ statusCode: 200 })
69+
70+
// assert that the original dispatch function was called with the request
71+
expect((originalEventDispatcher.dispatchEvent as unknown) as jest.Mock).toBeCalledTimes(1)
72+
expect(internalDispatchCall[0]).toEqual(eventV1Request)
73+
74+
// assert that the passed in callback to pendingEventsDispatcher was called
75+
expect(callback).toHaveBeenCalledTimes(1)
76+
expect(callback).toHaveBeenCalledWith({ statusCode: 200 })
77+
})
78+
79+
it('should properly send the events to the passed in eventDispatcher, when callback statusCode=400', () => {
80+
const callback = jest.fn()
81+
const eventV1Request: EventV1Request = {
82+
url: 'http://cdn.com',
83+
httpVerb: 'POST',
84+
params: ({ id: 'event' } as unknown) as EventV1,
85+
}
86+
87+
pendingEventsDispatcher.dispatchEvent(eventV1Request, callback)
88+
89+
expect(callback).not.toHaveBeenCalled()
90+
// manually invoke original eventDispatcher callback
91+
const internalDispatchCall = ((originalEventDispatcher.dispatchEvent as unknown) as jest.Mock)
92+
.mock.calls[0]
93+
internalDispatchCall[1]({ statusCode: 400 })
94+
95+
// assert that the original dispatch function was called with the request
96+
expect((originalEventDispatcher.dispatchEvent as unknown) as jest.Mock).toBeCalledTimes(1)
97+
expect(internalDispatchCall[0]).toEqual(eventV1Request)
98+
99+
// assert that the passed in callback to pendingEventsDispatcher was called
100+
expect(callback).toHaveBeenCalledTimes(1)
101+
expect(callback).toHaveBeenCalledWith({ statusCode: 400})
102+
})
103+
})
104+
105+
describe('PendingEventsDispatcher', () => {
106+
let originalEventDispatcher: EventDispatcher
107+
let pendingEventsDispatcher: PendingEventsDispatcher
108+
let store: PendingEventsStore<DispatcherEntry>
109+
110+
beforeEach(() => {
111+
originalEventDispatcher = {
112+
dispatchEvent: jest.fn(),
113+
}
114+
store = new LocalStorageStore({
115+
key: 'test',
116+
maxValues: 3,
117+
})
118+
pendingEventsDispatcher = new PendingEventsDispatcher({
119+
store,
120+
eventDispatcher: originalEventDispatcher,
121+
})
122+
;((getTimestamp as unknown) as jest.Mock).mockReturnValue(1)
123+
;((generateUUID as unknown) as jest.Mock).mockReturnValue('uuid')
124+
})
125+
126+
afterEach(() => {
127+
localStorage.clear()
128+
})
129+
130+
describe('dispatch', () => {
131+
describe('when the dispatch is successful', () => {
132+
it('should save the pendingEvent to the store and remove it once dispatch is completed', () => {
133+
const callback = jest.fn()
134+
const eventV1Request: EventV1Request = {
135+
url: 'http://cdn.com',
136+
httpVerb: 'POST',
137+
params: ({ id: 'event' } as unknown) as EventV1,
138+
}
139+
140+
pendingEventsDispatcher.dispatchEvent(eventV1Request, callback)
141+
142+
expect(store.values()).toHaveLength(1)
143+
expect(store.get('uuid')).toEqual({
144+
uuid: 'uuid',
145+
timestamp: 1,
146+
request: eventV1Request,
147+
})
148+
expect(callback).not.toHaveBeenCalled()
149+
150+
// manually invoke original eventDispatcher callback
151+
const internalDispatchCall = ((originalEventDispatcher.dispatchEvent as unknown) as jest.Mock)
152+
.mock.calls[0]
153+
const internalCallback = internalDispatchCall[1]({ statusCode: 200 })
154+
155+
// assert that the original dispatch function was called with the request
156+
expect(
157+
(originalEventDispatcher.dispatchEvent as unknown) as jest.Mock,
158+
).toBeCalledTimes(1)
159+
expect(internalDispatchCall[0]).toEqual(eventV1Request)
160+
161+
// assert that the passed in callback to pendingEventsDispatcher was called
162+
expect(callback).toHaveBeenCalledTimes(1)
163+
expect(callback).toHaveBeenCalledWith({ statusCode: 200 })
164+
165+
expect(store.values()).toHaveLength(0)
166+
})
167+
})
168+
169+
describe('when the dispatch is unsuccessful', () => {
170+
it('should save the pendingEvent to the store and remove it once dispatch is completed', () => {
171+
const callback = jest.fn()
172+
const eventV1Request: EventV1Request = {
173+
url: 'http://cdn.com',
174+
httpVerb: 'POST',
175+
params: ({ id: 'event' } as unknown) as EventV1,
176+
}
177+
178+
pendingEventsDispatcher.dispatchEvent(eventV1Request, callback)
179+
180+
expect(store.values()).toHaveLength(1)
181+
expect(store.get('uuid')).toEqual({
182+
uuid: 'uuid',
183+
timestamp: 1,
184+
request: eventV1Request,
185+
})
186+
expect(callback).not.toHaveBeenCalled()
187+
188+
// manually invoke original eventDispatcher callback
189+
const internalDispatchCall = ((originalEventDispatcher.dispatchEvent as unknown) as jest.Mock)
190+
.mock.calls[0]
191+
internalDispatchCall[1]({ statusCode: 400 })
192+
193+
// assert that the original dispatch function was called with the request
194+
expect(
195+
(originalEventDispatcher.dispatchEvent as unknown) as jest.Mock,
196+
).toBeCalledTimes(1)
197+
expect(internalDispatchCall[0]).toEqual(eventV1Request)
198+
199+
// assert that the passed in callback to pendingEventsDispatcher was called
200+
expect(callback).toHaveBeenCalledTimes(1)
201+
expect(callback).toHaveBeenCalledWith({ statusCode: 400 })
202+
203+
expect(store.values()).toHaveLength(0)
204+
})
205+
})
206+
})
207+
208+
describe('sendPendingEvents', () => {
209+
describe('when no pending events are in the store', () => {
210+
it('should not invoked dispatch', () => {
211+
expect(store.values()).toHaveLength(0)
212+
213+
pendingEventsDispatcher.sendPendingEvents()
214+
expect(originalEventDispatcher.dispatchEvent).not.toHaveBeenCalled()
215+
})
216+
})
217+
218+
describe('when there are multiple pending events in the store', () => {
219+
it('should dispatch all of the pending events, and remove them from store', () => {
220+
expect(store.values()).toHaveLength(0)
221+
222+
const callback = jest.fn()
223+
const eventV1Request1: EventV1Request = {
224+
url: 'http://cdn.com',
225+
httpVerb: 'POST',
226+
params: ({ id: 'event1' } as unknown) as EventV1,
227+
}
228+
229+
const eventV1Request2: EventV1Request = {
230+
url: 'http://cdn.com',
231+
httpVerb: 'POST',
232+
params: ({ id: 'event2' } as unknown) as EventV1,
233+
}
234+
235+
store.set('uuid1', {
236+
uuid: 'uuid1',
237+
timestamp: 1,
238+
request: eventV1Request1,
239+
})
240+
store.set('uuid2', {
241+
uuid: 'uuid2',
242+
timestamp: 2,
243+
request: eventV1Request2,
244+
})
245+
246+
expect(store.values()).toHaveLength(2)
247+
248+
pendingEventsDispatcher.sendPendingEvents()
249+
expect(originalEventDispatcher.dispatchEvent).toHaveBeenCalledTimes(2)
250+
251+
// manually invoke original eventDispatcher callback
252+
const internalDispatchCalls = ((originalEventDispatcher.dispatchEvent as unknown) as jest.Mock)
253+
.mock.calls
254+
internalDispatchCalls[0][1]({ statusCode: 200 })
255+
internalDispatchCalls[1][1]({ statusCode: 200 })
256+
257+
expect(store.values()).toHaveLength(0)
258+
})
259+
})
260+
})
261+
})

0 commit comments

Comments
 (0)