UNPKG

salsify-experiences-sdk

Version:

SDK to be used by commerce websites to implement product experiences.

196 lines (164 loc) 7.66 kB
import request from '../../request' import Transport from '../log-service-transport' import { makeContext, makeResponse, makeSettings } from '../../../__tests__/helpers' import { webcrypto } from 'crypto' import HttpStatus from 'http-status-codes' jest.mock('../../request') const flushPromises = (): Promise<void> => new Promise(process.nextTick) describe('log-service-transport', () => { beforeEach(() => { jest.useFakeTimers({ doNotFake: ['nextTick'] }) jest.spyOn(request, 'post').mockImplementation(async () => makeResponse('', HttpStatus.OK)) }) afterEach(() => { jest.resetAllMocks() jest.useRealTimers() }) test('should transform to log-service data model before sending', async () => { const context = makeContext() const settings = makeSettings() const transport = new Transport(context, settings) transport.log('foo', { bar: 'zaz' }, { url: 'example.com' }) await flushPromises() expect(request.post).toHaveBeenCalledTimes(1) const call = (request.post as jest.Mock).mock.calls[0] expect(call[0]).toBe('https://retail-client-events-service.internal.salsify.com/events') expect(call[1].app).toBe('sxp_sdk') expect(call[1].channel).toBe(context.clientId) expect(call[1].csid).toBe(context.sessionId) expect(call[1].pagesessionid).toBe(context.pageSessionId) expect(call[1].jsSource).toBe(context.jsSource) expect(typeof call[1].timestamp).toBe('number') expect(call[1].logs).toHaveLength(1) expect(call[1].logs[0].code).toBe('sdk_foo') expect(call[1].logs[0].properties.bar).toBe('zaz') expect(call[1].logs[0].context.url).toBe('example.com') expect(typeof call[1].logs[0].timestamp).toBe('number') }) test('should use new pageSessionId when updated', async () => { const context = makeContext() const settings = makeSettings() const transport = new Transport(context, settings) transport.log('foo', { bar: 'zaz' }, { url: 'example.com' }) await flushPromises() expect(request.post).toHaveBeenCalledTimes(1) const call = (request.post as jest.Mock).mock.calls[0] expect(call[1].pagesessionid).toBe(context.pageSessionId) const newPageSessionId = webcrypto.randomUUID() context.pageSessionId = newPageSessionId transport.log('foo', { bar: 'zaz' }, { url: 'example.com' }) await flushPromises() expect(request.post).toHaveBeenCalledTimes(2) const call2 = (request.post as jest.Mock).mock.calls[1] expect(call2[1].pagesessionid).toBe(newPageSessionId) }) test('should send to staging when configured', async () => { const context = makeContext() const settings = makeSettings({ staging: true }) const transport = new Transport(context, settings) transport.log('foo', { bar: 'zaz' }, { url: 'example.com' }) await flushPromises() expect(request.post).toHaveBeenCalledTimes(1) const call = (request.post as jest.Mock).mock.calls[0] expect(call[0]).toBe('https://retail-client-events-service-staging.internal.salsify.com/events') }) test('it should resend failed events on network error', async () => { const context = makeContext() const settings = makeSettings() const transport = new Transport(context, settings) const postSpy = jest.spyOn(request, 'post').mockRejectedValue(new Error()) transport.log('foo', { bar: 'zaz' }, { url: 'example.com' }) expect(request.post).toHaveBeenCalledTimes(1) expect(postSpy.mock.calls[0][1].logs).toEqual([expect.objectContaining({ code: 'sdk_foo' })]) await flushPromises() transport.log('foo2', { bar: 'zaz2' }, { url: 'example2.com' }) expect(request.post).toHaveBeenCalledTimes(2) await flushPromises() jest.advanceTimersByTime(5000) // All failed events are included in retry expect(request.post).toHaveBeenCalledTimes(3) expect(postSpy.mock.calls[2][1].logs).toEqual([ expect.objectContaining({ code: 'sdk_foo' }), expect.objectContaining({ code: 'sdk_foo2' }), ]) await flushPromises() jest.advanceTimersByTime(20000) // Retries happen with exponential backoff expect(request.post).toHaveBeenCalledTimes(4) expect(postSpy.mock.calls[3][1].logs).toEqual([ expect.objectContaining({ code: 'sdk_foo' }), expect.objectContaining({ code: 'sdk_foo2' }), ]) await flushPromises() }) test('it should resend failed events on HTTP error', async () => { const context = makeContext() const settings = makeSettings() const transport = new Transport(context, settings) const postSpy = jest .spyOn(request, 'post') .mockImplementation(async () => makeResponse('', HttpStatus.SERVICE_UNAVAILABLE)) transport.log('foo', { bar: 'zaz' }, { url: 'example.com' }) expect(request.post).toHaveBeenCalledTimes(1) expect(postSpy.mock.calls[0][1].logs).toEqual([expect.objectContaining({ code: 'sdk_foo' })]) await flushPromises() transport.log('foo2', { bar: 'zaz2' }, { url: 'example2.com' }) expect(request.post).toHaveBeenCalledTimes(2) await flushPromises() jest.advanceTimersByTime(5000) // All failed events are included in retry expect(request.post).toHaveBeenCalledTimes(3) expect(postSpy.mock.calls[2][1].logs).toEqual([ expect.objectContaining({ code: 'sdk_foo' }), expect.objectContaining({ code: 'sdk_foo2' }), ]) await flushPromises() jest.advanceTimersByTime(20000) // Retries happen with exponential backoff expect(request.post).toHaveBeenCalledTimes(4) expect(postSpy.mock.calls[3][1].logs).toEqual([ expect.objectContaining({ code: 'sdk_foo' }), expect.objectContaining({ code: 'sdk_foo2' }), ]) await flushPromises() }) test('it should clear successfully resent events from the queue and reset the timer', async () => { const context = makeContext() const settings = makeSettings() const transport = new Transport(context, settings) jest.spyOn(request, 'post').mockRejectedValueOnce(new Error()) transport.log('foo', { bar: 'zaz' }, { url: 'example.com' }) await flushPromises() expect(request.post).toHaveBeenCalledTimes(1) jest.advanceTimersByTime(5000) await flushPromises() expect(request.post).toHaveBeenCalledTimes(2) jest.advanceTimersByTime(20000) await flushPromises() expect(request.post).toHaveBeenCalledTimes(2) jest.spyOn(request, 'post').mockImplementationOnce(async () => makeResponse('', HttpStatus.SERVICE_UNAVAILABLE)) transport.log('foo', { bar: 'zaz' }, { url: 'example.com' }) await flushPromises() expect(request.post).toHaveBeenCalledTimes(3) jest.advanceTimersByTime(5000) await flushPromises() expect(request.post).toHaveBeenCalledTimes(4) }) test('it should still resend events from the queue even if the timer is cleared before going off', async () => { const context = makeContext() const settings = makeSettings() const transport = new Transport(context, settings) const postSpy = jest.spyOn(request, 'post').mockRejectedValueOnce(new Error()) transport.log('foo', { bar: 'zaz' }, { url: 'example.com' }) expect(request.post).toHaveBeenCalledTimes(1) expect(postSpy.mock.calls[0][1].logs).toEqual([expect.objectContaining({ code: 'sdk_foo' })]) await flushPromises() transport.log('foo2', { bar: 'zaz2' }, { url: 'example2.com' }) expect(request.post).toHaveBeenCalledTimes(2) expect(postSpy.mock.calls[1][1].logs).toEqual([ expect.objectContaining({ code: 'sdk_foo' }), expect.objectContaining({ code: 'sdk_foo2' }), ]) await flushPromises() }) })