import { UA_REGEX, UUID_REGEX } from '../../../../tests/shared'; import { ReportItemType } from '../common'; import { TestAwaitedTransport, TestCollector, TestReporter, TestTransport } from './utils'; describe('Reporter', () => { beforeEach(() => { jest.resetAllMocks(); jest.useFakeTimers(); }); afterEach(() => { jest.useRealTimers(); localStorage.clear(); }); afterAll(() => { jest.resetAllMocks(); }); it('construct with correct default options value', () => { const reporter = new TestReporter() as any; expect(reporter.userInfo).toEqual({}); expect(reporter.debug).toBe(false); expect(reporter.errorHandler).toEqual(expect.any(Function)); }); it('can setOptions with custom options correctly', () => { const reporter = new TestReporter() as any; reporter.setOptions({ debug: true, }); expect(reporter.userInfo).toEqual({}); expect(reporter.debug).toBe(true); expect(reporter.errorHandler).toEqual(expect.any(Function)); const reporter2 = new TestReporter() as any; reporter2.setOptions({ debug: false, errorHandler: () => {}, aIdGenerator: () => 'JoJo', collectors: [new TestCollector()], transports: [new TestTransport()], }); expect(reporter2.userInfo).toEqual({}); expect(reporter2.debug).toBe(false); expect(reporter2.errorHandler).toEqual(expect.any(Function)); expect(reporter2.aIdGenerator()).toBe('JoJo'); }); it('setUserInfo correctly', () => { const reporter = new TestReporter() as any; reporter.setUserInfo({ uin: 999, extra: { name: 'Alice', }, }); expect(reporter.userInfo).toStrictEqual({ uin: 999, extra: { name: 'Alice', }, }); }); it('init correctly', () => { // TODO: 未 init 时执行其他下游方法要拒绝的 const reporter = new TestReporter() as any; const flushSpy = jest.spyOn(reporter, 'flush'); // TODO: 允许多次 init 吗? reporter.init({ adapter: 'web', user: { uin: 123123123, }, }); expect(reporter.userInfo).toEqual({ aid: expect.stringMatching(UUID_REGEX), uin: 123123123, }); jest.advanceTimersByTime(3100); // flush 现在仅在 report 和 settle 等情况会被调用,不再由计时器控制 expect(flushSpy).not.toHaveBeenCalled(); }); it('allows init with undefined collectors or transports', () => { const reporter = new TestReporter() as any; reporter.init({ transports: [undefined, { a: 1 }, new TestTransport(), undefined, true], collectors: [ undefined, { a: 1 }, { init: 1, destroy: 1, settle: 1 }, new TestCollector(), undefined, true, { init: () => {}, destroy: () => {}, settle: () => {} }, // 只要满足校验就可以 ], }); expect(reporter.transports.length).toBe(1); expect(reporter.collectors.length).toBe(2); }); it('can get correct aid after init', () => { const reporter = new TestReporter() as any; expect(reporter.userInfo.aid).toBeUndefined(); reporter.init({}); expect(reporter.userInfo.aid).toMatch(UUID_REGEX); }); it('will use aid in localStorage first', () => { const reporter = new TestReporter() as any; reporter.init({}); const oaid = reporter.userInfo.aid; expect(oaid).toBeTruthy(); const reporterWithCustomAIdGenerator = new TestReporter() as any; reporterWithCustomAIdGenerator.init({ aIdGenerator: () => 'JoJo', }); expect(reporterWithCustomAIdGenerator.userInfo.aid).toBe(oaid); }); it('will regenerate aid if no aid in localStorage', () => { const reporter = new TestReporter() as any; reporter.init({}); const oaid = reporter.userInfo.aid; expect(oaid).toBeTruthy(); localStorage.clear(); const reporterWithCustomAIdGenerator = new TestReporter() as any; reporterWithCustomAIdGenerator.init({ aIdGenerator: () => 'JoJo', }); expect(reporterWithCustomAIdGenerator.userInfo.aid).toBe('JoJo'); }); test('baseContext works ok', () => { const reporter = new TestReporter() as any; expect(reporter.baseContext).toStrictEqual({ release: undefined, user: {}, }); reporter.init({ adapter: 'web', user: { aid: '1', uin: 123123123, }, release: 'v2.3.3', }); expect(reporter.baseContext).toStrictEqual({ user: { aid: expect.stringMatching(UUID_REGEX), // 此处不为 1,aid 总是自动生成的 uin: 123123123, }, release: 'v2.3.3', }); const { aid } = reporter.userInfo; reporter.setUserInfo({ uin: 999, extra: { name: 'Alice', }, }); expect(reporter.baseContext).toStrictEqual({ user: { aid, uin: 999, extra: { name: 'Alice', }, }, release: 'v2.3.3', }); }); test('log works correctly', () => { const outputContent: Parameters[] = []; jest.spyOn(console, 'log').mockImplementation((...args: Parameters) => { outputContent.push(args); }); // no debug mode, no log const reporter = new TestReporter(); reporter.log('test'); reporter.log(); reporter.log('test', 'test'); expect(outputContent).toEqual([]); const reporterInDebugMode = new TestReporter(); reporterInDebugMode.setOptions({ debug: true, }); reporterInDebugMode.log('test'); reporterInDebugMode.log(); reporterInDebugMode.log('test', 'test'); expect(outputContent).toEqual([['[Merlin]', 'test'], ['[Merlin]'], ['[Merlin]', 'test', 'test']]); }); it('getContext correctly', () => { const reporter = new TestReporter() as any; expect(reporter.getContext()).toStrictEqual({ base: { release: undefined, user: {}, }, env: {}, scope: { contextId: expect.stringMatching(UUID_REGEX), }, }); }); it('sendToTransports correctly when immediate is true', async () => { /** send 立刻执行 */ const transportWithoutDelay = new TestTransport(); /** send 耗时 1s */ const transportWith1sDelay = new TestAwaitedTransport(1000); /** send 耗时 2s */ const transportWith2sDelay = new TestAwaitedTransport(2000); const transportWithoutDelaySendSpy = jest.spyOn(transportWithoutDelay, 'send'); const transportWith1sDelaySendSpy = jest.spyOn(transportWith1sDelay, 'send'); const transportWith2sDelaySendSpy = jest.spyOn(transportWith2sDelay, 'send'); const reporter = new TestReporter() as any; reporter.setOptions({ transports: [transportWith1sDelay, transportWithoutDelay, transportWith2sDelay], }); const records = [ { type: ReportItemType.BEHAVIOR, data: {}, ctime: 123, context: { scope: { pageName: 'Nezuko', accessId: '1', }, base: { user: {}, }, env: {}, }, }, ]; const promise = reporter.sendToTransports(records, true); jest.runAllTimers(); const res = await promise; expect(transportWithoutDelaySendSpy).toHaveBeenCalledTimes(1); expect(transportWith1sDelaySendSpy).toHaveBeenCalledTimes(1); expect(transportWith2sDelaySendSpy).toHaveBeenCalledTimes(1); expect(transportWithoutDelay.consume()).toEqual(records); expect(transportWith1sDelay.consume()).toEqual(records); expect(transportWith2sDelay.consume()).toEqual(records); expect(res).toEqual([undefined, undefined, undefined]); }); it('sendToTransports correctly when immediate is false', async () => { /** send 立刻执行 */ const transportWithoutDelay = new TestTransport(); /** send 耗时 1s */ const transportWith1sDelay = new TestAwaitedTransport(1000); /** send 耗时 2s */ const transportWith2sDelay = new TestAwaitedTransport(2000); const transportWithoutDelaySendSpy = jest.spyOn(transportWithoutDelay, 'send'); const transportWith1sDelaySendSpy = jest.spyOn(transportWith1sDelay, 'send'); const transportWith2sDelaySendSpy = jest.spyOn(transportWith2sDelay, 'send'); const reporter = new TestReporter() as any; reporter.setOptions({ transports: [transportWith1sDelay, transportWithoutDelay, transportWith2sDelay], }); const records = [ { type: ReportItemType.BEHAVIOR, data: {}, ctime: 123, context: { scope: { pageName: 'Nezuko', accessId: '1', }, base: { user: {}, }, env: {}, }, }, ]; // 不立刻发送的情况下,只会推到 Transports 的缓存中,不会触发 send const promise = reporter.sendToTransports(records); jest.runAllTimers(); const res = await promise; expect(transportWithoutDelaySendSpy).not.toHaveBeenCalled(); expect(transportWith1sDelaySendSpy).not.toHaveBeenCalled(); expect(transportWith2sDelaySendSpy).not.toHaveBeenCalled(); expect(transportWithoutDelay.consume()).toEqual([]); expect(transportWith1sDelay.consume()).toEqual([]); expect(transportWith2sDelay.consume()).toEqual([]); expect(res).toEqual([undefined, undefined, undefined]); // 但它的 buffer 里有 expect((transportWithoutDelay as any).buffer).toEqual(records); // 此时调用 flush(immediate: true),会立刻发送 const promise2 = reporter.flush(true); jest.runAllTimers(); await promise2; expect(transportWithoutDelaySendSpy).toHaveBeenCalledTimes(1); expect(transportWith1sDelaySendSpy).toHaveBeenCalledTimes(1); expect(transportWith2sDelaySendSpy).toHaveBeenCalledTimes(1); expect(transportWithoutDelay.consume()).toEqual(records); expect(transportWith1sDelay.consume()).toEqual(records); expect(transportWith2sDelay.consume()).toEqual(records); expect((transportWithoutDelay as any).buffer).toEqual([]); }); test('report and flush correctly', () => { /** 流程设计 * 1. 初始化:5s 一次,Transport 上限 20 * 2. 过 5s 不操作,没有数据,不该上报出去 * 3. 加入 2 条数据后等 5s,应该触发上报 * 4. 直接塞入 50 条数据,应该立刻触发两次上报 * 5. 过 5s,应该会把剩余 10 条都发出 */ const transport1 = new TestTransport({ bufferSize: 20, flushInterval: 5000, }); const transport2 = new TestTransport({ bufferSize: 20, flushInterval: 5000, }); const reporter = new TestReporter() as any; const transport1SendSpy = jest.spyOn(transport1, 'send'); const transport2SendSpy = jest.spyOn(transport2, 'send'); const flushSpy = jest.spyOn(reporter, 'flush'); jest.spyOn(Date, 'now').mockImplementation(() => 1638374400000); reporter.init({ user: { uin: 9999, }, transports: [transport1, transport2], }); // 初始化后等五秒不进行 report,不应该触发 transport 的 send jest.advanceTimersByTime(5100); expect(flushSpy).toHaveBeenCalledTimes(0); expect(transport1SendSpy).toHaveBeenCalledTimes(0); expect(transport2SendSpy).toHaveBeenCalledTimes(0); const record = { type: ReportItemType.BEHAVIOR, data: {}, }; // report 两项,不会立刻触发上报 reporter.report(record); reporter.report(record); expect(flushSpy).toHaveBeenCalledTimes(2); expect(transport1SendSpy).toHaveBeenCalledTimes(0); expect(transport2SendSpy).toHaveBeenCalledTimes(0); expect((reporter as any).buffer.length).toBe(0); expect((transport1 as any).buffer.length).toBe(2); expect((transport2 as any).buffer.length).toBe(2); // 五秒后这两项应该报上去了 jest.advanceTimersByTime(5100); expect(flushSpy).toHaveBeenCalledTimes(2); expect(transport1SendSpy).toHaveBeenCalledTimes(1); expect(transport2SendSpy).toHaveBeenCalledTimes(1); expect((reporter as any).buffer.length).toBe(0); expect(transport1.consume().length).toEqual(2); expect(transport2.consume().length).toEqual(2); // 连续 report 五十项,其间应该会触发 2 次超限上报,每次报 20 条,留下 10 条数据 for (let i = 0; i < 50; i++) { reporter.report(record); } expect(flushSpy).toHaveBeenCalledTimes(52); expect(transport1SendSpy).toHaveBeenCalledTimes(3); expect(transport2SendSpy).toHaveBeenCalledTimes(3); expect((reporter as any).buffer.length).toBe(0); expect((transport1 as any).buffer.length).toBe(10); expect((transport2 as any).buffer.length).toBe(10); expect(transport1.consume().length).toEqual(40); expect(transport2.consume().length).toEqual(40); // 5 秒后,这 10 条数据也应该上报了 jest.advanceTimersByTime(5000); expect(flushSpy).toHaveBeenCalledTimes(52); expect(transport1SendSpy).toHaveBeenCalledTimes(4); expect(transport2SendSpy).toHaveBeenCalledTimes(4); expect((reporter as any).buffer.length).toBe(0); expect(transport1.consume().length).toEqual(10); const transport2FinalConsume = transport2.consume(); expect(transport2FinalConsume.length).toEqual(10); expect(transport2FinalConsume?.[0]).toStrictEqual({ ctime: 1638374400000, type: ReportItemType.BEHAVIOR, data: {}, context: { base: { release: undefined, user: { aid: expect.stringMatching(UUID_REGEX), uin: 9999, }, }, env: { userAgent: expect.stringMatching(UA_REGEX), }, scope: { clientHeight: 768, clientWidth: 1024, contextId: expect.stringMatching(UUID_REGEX), devicePixelRatio: 1, screenHeight: 0, screenWidth: 0, href: 'http://localhost/', }, }, }); }); it('enableBuffering correctly', () => { const reporter = new TestReporter() as any; expect(reporter.buffering).toBe(false); reporter.enableBuffering(); expect(reporter.buffering).toBe(true); }); it("won't trigger sendToTransports if is buffering", () => { const reporter = new TestReporter() as any; const sendToTransportsSpy = jest.spyOn(reporter, 'sendToTransports'); reporter.init({ adapter: 'web', user: { uin: 9999, }, }); reporter.enableBuffering(); const record = { type: ReportItemType.BEHAVIOR, data: {}, }; reporter.report(record); reporter.report(record); reporter.report(record); reporter.report(record); reporter.report(record); expect(sendToTransportsSpy).toHaveBeenCalledTimes(0); }); test('settleBuffering change buffering correctly', () => { const reporter = new TestReporter(); reporter.enableBuffering(); expect((reporter as any).buffering).toBe(true); reporter.settleBuffering(); expect((reporter as any).buffering).toBe(false); }); test('settleBuffering will send all buffered records to transports', async () => { const transport = new TestTransport({ bufferSize: 20, flushInterval: 0, }); const reporter = new TestReporter() as any; const flushSpy = jest.spyOn(reporter, 'flush'); const sendToTransportsSpy = jest.spyOn(reporter, 'sendToTransports'); reporter.init({ adapter: 'web', transports: [transport], user: { uin: 9999, }, }); reporter.enableBuffering(); const record = { type: ReportItemType.BEHAVIOR, data: {}, }; reporter.report(record); reporter.report(record); reporter.report(record); reporter.report(record); reporter.report(record); expect(flushSpy).toHaveBeenCalledTimes(5); // 在 report 中被触发,但不会实际 sendToTransports expect(sendToTransportsSpy).toHaveBeenCalledTimes(0); const promise = reporter.settleBuffering(); jest.runAllTimers(); await promise; expect(flushSpy).toHaveBeenCalledTimes(6); expect(sendToTransportsSpy).toHaveBeenCalledTimes(1); expect((transport as any).buffer.length).toBe(5); }); test('buffering mechanism works well', () => { /** 流程设计 * 1. 初始化:5s 一次,bufferSize 5 * 2. 推入 5 条,应该立刻触发上报 * 3. enableBuffering * 4. 推入 6 条,等 5s,应该一直不上报 * 5. settleBuffering,应该立刻触发上报 * 6. 推入 1 条,等 5s,应该正常触发上报 */ const transport = new TestTransport({ bufferSize: 5, flushInterval: 5000, }); const reporter = new TestReporter(); const transportSendSpy = jest.spyOn(transport, 'send'); jest.spyOn(Date, 'now').mockImplementation(() => 1638374400000); reporter.init({ adapter: 'web', user: { uin: 9999, }, transports: [transport], }); const record = { type: ReportItemType.BEHAVIOR, data: {}, }; // 推入 5 条,应该立刻触发上报 reporter.report(record); reporter.report(record); reporter.report(record); reporter.report(record); reporter.report(record); expect(transportSendSpy).toHaveBeenCalledTimes(1); expect(transport.consume().length).toBe(5); reporter.enableBuffering(); // 推入 6 条,等 5s,应该一直不上报 reporter.report(record); reporter.report(record); reporter.report(record); reporter.report(record); reporter.report(record); reporter.report(record); jest.advanceTimersByTime(5100); expect(transportSendSpy).toHaveBeenCalledTimes(1); expect(transport.consume().length).toBe(0); // settleBuffering,应该立刻触发上报 reporter.settleBuffering(); expect(transportSendSpy).toHaveBeenCalledTimes(2); expect(transport.consume().length).toBe(6); // 推入 1 条,等 5s,应该正常触发上报 reporter.report(record); jest.advanceTimersByTime(5100); expect(transportSendSpy).toHaveBeenCalledTimes(3); expect(transport.consume().length).toBe(1); }); });