From a42a626e4db04aca25dd32701a89dd3cb62726fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?I=CC=81caro?= Date: Wed, 1 Sep 2021 19:12:30 -0300 Subject: [PATCH 01/14] fix: general wrapper types for analytics and removed unecessary code from unwrap function --- packages/store-sdk/src/analytics/index.ts | 46 +++++++++++-------- .../src/analytics/useAnalyticsEvent.ts | 8 +++- packages/store-sdk/src/index.ts | 2 +- 3 files changed, 33 insertions(+), 23 deletions(-) diff --git a/packages/store-sdk/src/analytics/index.ts b/packages/store-sdk/src/analytics/index.ts index a96a967d8b..574fa15633 100644 --- a/packages/store-sdk/src/analytics/index.ts +++ b/packages/store-sdk/src/analytics/index.ts @@ -40,34 +40,40 @@ export type AnalyticsEvent = | SignupEvent | ShareEvent -export type WrappedAnalyticsEvent = { +export type WrappedAnalyticsEventData = Omit< + T, + 'type' +> & { + type: `store:${T['type']}` +} + +export interface WrappedAnalyticsEvent { type: 'AnalyticsEvent' - data: T + data: WrappedAnalyticsEventData } export const STORE_EVENT_PREFIX = 'store:' +export const ANALYTICS_EVENT_TYPE = 'AnalyticsEvent' export const wrap = ( event: T -): WrappedAnalyticsEvent => ({ - type: 'AnalyticsEvent', - data: { - ...event, - type: `${STORE_EVENT_PREFIX}${event.type}`, - }, -}) +): WrappedAnalyticsEvent => + ({ + type: ANALYTICS_EVENT_TYPE, + data: { + ...event, + type: `${STORE_EVENT_PREFIX}${event.type}` as const, + }, + } as WrappedAnalyticsEvent) export const unwrap = ( event: WrappedAnalyticsEvent -) => { - if (event.type === 'AnalyticsEvent') { - return { - ...event.data, - type: event.type.startsWith(STORE_EVENT_PREFIX) - ? event.type.slice(STORE_EVENT_PREFIX.length, event.type.length) - : event.type, - } - } - - return null +): T => { + return { + ...event.data, + type: event.data.type.slice( + STORE_EVENT_PREFIX.length, + event.data.type.length + ), + } as T } diff --git a/packages/store-sdk/src/analytics/useAnalyticsEvent.ts b/packages/store-sdk/src/analytics/useAnalyticsEvent.ts index b499e6df75..174adabf55 100644 --- a/packages/store-sdk/src/analytics/useAnalyticsEvent.ts +++ b/packages/store-sdk/src/analytics/useAnalyticsEvent.ts @@ -1,6 +1,6 @@ import { useCallback, useEffect } from 'react' -import { unwrap } from '.' +import { ANALYTICS_EVENT_TYPE, unwrap } from '.' import type { AnalyticsEvent } from '.' export type AnalyticsEventHandler = ( @@ -11,7 +11,11 @@ export const useAnalyticsEvent = (handler: AnalyticsEventHandler) => { const callback = useCallback( (message: MessageEvent) => { try { - const maybeEvent = unwrap(message.data ?? {}) + if (message.data.type !== ANALYTICS_EVENT_TYPE) { + return + } + + const maybeEvent = unwrap(message.data) if (maybeEvent) { handler(maybeEvent) diff --git a/packages/store-sdk/src/index.ts b/packages/store-sdk/src/index.ts index 271fba67b7..db08ee6cd2 100644 --- a/packages/store-sdk/src/index.ts +++ b/packages/store-sdk/src/index.ts @@ -59,7 +59,7 @@ export type { CurrencyCode, } from './analytics/events/common' export type { AnalyticsEvent, WrappedAnalyticsEvent } from './analytics/index' -export { STORE_EVENT_PREFIX } from './analytics/index' +export { STORE_EVENT_PREFIX, ANALYTICS_EVENT_TYPE } from './analytics/index' export { sendAnalyticsEvent } from './analytics/sendAnalyticsEvent' export type { AnalyticsEventHandler } from './analytics/useAnalyticsEvent' export { useAnalyticsEvent } from './analytics/useAnalyticsEvent' From 9b1fb0aa6a62fc4235a0612ede25c798ae2ac5b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?I=CC=81caro?= Date: Wed, 1 Sep 2021 19:15:15 -0300 Subject: [PATCH 02/14] chore: renaming analytics index file to wrap --- packages/store-sdk/src/analytics/sendAnalyticsEvent.ts | 4 ++-- packages/store-sdk/src/analytics/useAnalyticsEvent.ts | 4 ++-- packages/store-sdk/src/analytics/{index.ts => wrap.ts} | 0 packages/store-sdk/src/index.ts | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) rename packages/store-sdk/src/analytics/{index.ts => wrap.ts} (100%) diff --git a/packages/store-sdk/src/analytics/sendAnalyticsEvent.ts b/packages/store-sdk/src/analytics/sendAnalyticsEvent.ts index bb049b6d91..e8b5be1bec 100644 --- a/packages/store-sdk/src/analytics/sendAnalyticsEvent.ts +++ b/packages/store-sdk/src/analytics/sendAnalyticsEvent.ts @@ -1,5 +1,5 @@ -import { wrap } from '.' -import type { AnalyticsEvent } from '.' +import { wrap } from './wrap' +import type { AnalyticsEvent } from './wrap' export const sendAnalyticsEvent = (event: AnalyticsEvent) => { try { diff --git a/packages/store-sdk/src/analytics/useAnalyticsEvent.ts b/packages/store-sdk/src/analytics/useAnalyticsEvent.ts index 174adabf55..d474242d6e 100644 --- a/packages/store-sdk/src/analytics/useAnalyticsEvent.ts +++ b/packages/store-sdk/src/analytics/useAnalyticsEvent.ts @@ -1,7 +1,7 @@ import { useCallback, useEffect } from 'react' -import { ANALYTICS_EVENT_TYPE, unwrap } from '.' -import type { AnalyticsEvent } from '.' +import { ANALYTICS_EVENT_TYPE, unwrap } from './wrap' +import type { AnalyticsEvent } from './wrap' export type AnalyticsEventHandler = ( event: AnalyticsEvent diff --git a/packages/store-sdk/src/analytics/index.ts b/packages/store-sdk/src/analytics/wrap.ts similarity index 100% rename from packages/store-sdk/src/analytics/index.ts rename to packages/store-sdk/src/analytics/wrap.ts diff --git a/packages/store-sdk/src/index.ts b/packages/store-sdk/src/index.ts index db08ee6cd2..a4c2f8226e 100644 --- a/packages/store-sdk/src/index.ts +++ b/packages/store-sdk/src/index.ts @@ -58,8 +58,8 @@ export type { PromotionItem, CurrencyCode, } from './analytics/events/common' -export type { AnalyticsEvent, WrappedAnalyticsEvent } from './analytics/index' -export { STORE_EVENT_PREFIX, ANALYTICS_EVENT_TYPE } from './analytics/index' +export type { AnalyticsEvent, WrappedAnalyticsEvent } from './analytics/wrap' +export { STORE_EVENT_PREFIX, ANALYTICS_EVENT_TYPE } from './analytics/wrap' export { sendAnalyticsEvent } from './analytics/sendAnalyticsEvent' export type { AnalyticsEventHandler } from './analytics/useAnalyticsEvent' export { useAnalyticsEvent } from './analytics/useAnalyticsEvent' From 0175ac30e0374d43786cef0ef275ddc93151ddad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?I=CC=81caro?= Date: Wed, 1 Sep 2021 19:16:18 -0300 Subject: [PATCH 03/14] chore: adding wrap and unwrap functions tests --- .../store-sdk/test/analytics/wrap.test.ts | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 packages/store-sdk/test/analytics/wrap.test.ts diff --git a/packages/store-sdk/test/analytics/wrap.test.ts b/packages/store-sdk/test/analytics/wrap.test.ts new file mode 100644 index 0000000000..ee09366bb5 --- /dev/null +++ b/packages/store-sdk/test/analytics/wrap.test.ts @@ -0,0 +1,43 @@ +// import React from 'react' +// import { act, renderHook } from '@testing-library/react-hooks' + +import { wrap, unwrap } from '../../src/analytics/wrap' +import type { AddToCartEvent } from '../../src/analytics/events/add_to_cart' + +const eventSample: AddToCartEvent = { + type: 'add_to_cart', + data: { + items: [{ item_id: 'PRODUCT_ID' }], + }, +} + +describe('wrap and unwrap functions', () => { + it('wrap function wraps event with AnalyticsEvent type', () => { + const { type } = wrap(eventSample) + + expect(type).toBe('AnalyticsEvent') + }) + + it('wrap function prefixes the event type', () => { + const wrappedEvent = wrap(eventSample) + const { type: wrappedEventType } = wrappedEvent.data + const { type: originalType } = eventSample + + expect(wrappedEventType).toBe(`store:${originalType}`) + }) + + it('wrap function preserves all event data but the type', () => { + const wrappedEvent = wrap(eventSample) + const { type: _, ...wrappedEventRest } = wrappedEvent.data + const { type: __, ...originalEventRest } = eventSample + + expect(wrappedEventRest).toStrictEqual(originalEventRest) + }) + + it('unwrap function preserves the original event', () => { + const wrappedEvent = wrap(eventSample) + const unwrappedEvent = unwrap(wrappedEvent) ?? {} + + expect(unwrappedEvent).toStrictEqual(eventSample) + }) +}) From 3efef0b58e934e94ccc738e20cce698b21ced40b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?I=CC=81caro?= Date: Thu, 2 Sep 2021 15:10:29 -0300 Subject: [PATCH 04/14] chore: adding tests to analytics layer --- .../src/analytics/useAnalyticsEvent.ts | 6 +-- .../test/analytics/sendAnalyticsEvent.test.ts | 41 +++++++++++++++ .../test/analytics/useAnalyticsEvent.test.ts | 51 +++++++++++++++++++ .../store-sdk/test/analytics/wrap.test.ts | 3 -- 4 files changed, 93 insertions(+), 8 deletions(-) create mode 100644 packages/store-sdk/test/analytics/sendAnalyticsEvent.test.ts create mode 100644 packages/store-sdk/test/analytics/useAnalyticsEvent.test.ts diff --git a/packages/store-sdk/src/analytics/useAnalyticsEvent.ts b/packages/store-sdk/src/analytics/useAnalyticsEvent.ts index d474242d6e..ead7af0607 100644 --- a/packages/store-sdk/src/analytics/useAnalyticsEvent.ts +++ b/packages/store-sdk/src/analytics/useAnalyticsEvent.ts @@ -15,11 +15,7 @@ export const useAnalyticsEvent = (handler: AnalyticsEventHandler) => { return } - const maybeEvent = unwrap(message.data) - - if (maybeEvent) { - handler(maybeEvent) - } + handler(unwrap(message.data)) } catch (err) { console.error('Some bad happened while running Analytics handler') } diff --git a/packages/store-sdk/test/analytics/sendAnalyticsEvent.test.ts b/packages/store-sdk/test/analytics/sendAnalyticsEvent.test.ts new file mode 100644 index 0000000000..8b0c83cfb3 --- /dev/null +++ b/packages/store-sdk/test/analytics/sendAnalyticsEvent.test.ts @@ -0,0 +1,41 @@ +import { sendAnalyticsEvent } from '../../src/analytics/sendAnalyticsEvent' +import type { AddToCartEvent } from '../../src/analytics/events/add_to_cart' +import type { WrappedAnalyticsEvent } from '../../src/analytics/wrap' + +const eventSample: AddToCartEvent = { + type: 'add_to_cart', + data: { + items: [{ item_id: 'PRODUCT_ID' }], + }, +} + +const wrappedEventSample: WrappedAnalyticsEvent = { + type: 'AnalyticsEvent', + data: { + type: 'store:add_to_cart', + data: { + items: [{ item_id: 'PRODUCT_ID' }], + }, + }, +} + +const noop = () => {} +const origin = 'http://localhost:8080/' + +describe('sendAnalyticsEvent', () => { + it('window.postMessage is called with correct params', () => { + const postMessageSpy = jest + .spyOn(window, 'postMessage') + .mockImplementation(noop) + + Object.defineProperty(window, 'origin', { + writable: false, + value: origin, + }) + + sendAnalyticsEvent(eventSample) + + expect(postMessageSpy).toHaveBeenCalled() + expect(postMessageSpy).toHaveBeenCalledWith(wrappedEventSample, origin) + }) +}) diff --git a/packages/store-sdk/test/analytics/useAnalyticsEvent.test.ts b/packages/store-sdk/test/analytics/useAnalyticsEvent.test.ts new file mode 100644 index 0000000000..6b9f569dcb --- /dev/null +++ b/packages/store-sdk/test/analytics/useAnalyticsEvent.test.ts @@ -0,0 +1,51 @@ +import { renderHook } from '@testing-library/react-hooks' + +import { useAnalyticsEvent } from '../../src/analytics/useAnalyticsEvent' +import { wrap } from '../../src/analytics/wrap' +import type { AddToCartEvent } from '../../src/analytics/events/add_to_cart' + +const eventSample: AddToCartEvent = { + type: 'add_to_cart', + data: { + items: [{ item_id: 'PRODUCT_ID' }], + }, +} + +describe('useAnalyticsEvent', () => { + afterAll(() => { + jest.restoreAllMocks() + }) + + it('useAnalyticsEvent calls handler with correct params when an AnalyticsEvent is fired', async () => { + const handler = jest.fn() + + jest.spyOn(window, 'addEventListener').mockImplementation((_, fn) => { + if (typeof fn === 'function') { + fn(new MessageEvent('message', { data: wrap(eventSample) })) + } + }) + + renderHook(() => useAnalyticsEvent(handler)) + + expect(handler).toHaveBeenCalled() + expect(handler).toHaveBeenCalledWith(eventSample) + }) + + it('useAnalyticsEvent ignores events that are not AnalyticsEvent', async () => { + const handler = jest.fn() + + jest.spyOn(window, 'addEventListener').mockImplementation((_, fn) => { + if (typeof fn === 'function') { + fn( + new MessageEvent('message', { + data: { ...wrap(eventSample), type: 'OtherEventType' }, + }) + ) + } + }) + + renderHook(() => useAnalyticsEvent(handler)) + + expect(handler).not.toHaveBeenCalled() + }) +}) diff --git a/packages/store-sdk/test/analytics/wrap.test.ts b/packages/store-sdk/test/analytics/wrap.test.ts index ee09366bb5..091d02ed3b 100644 --- a/packages/store-sdk/test/analytics/wrap.test.ts +++ b/packages/store-sdk/test/analytics/wrap.test.ts @@ -1,6 +1,3 @@ -// import React from 'react' -// import { act, renderHook } from '@testing-library/react-hooks' - import { wrap, unwrap } from '../../src/analytics/wrap' import type { AddToCartEvent } from '../../src/analytics/events/add_to_cart' From 848819559e6a89ae81a29086859404a2fd53a15b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?I=CC=81caro?= Date: Mon, 6 Sep 2021 13:15:30 -0300 Subject: [PATCH 05/14] feat: allows sending custom analytics events --- .../store-sdk/src/analytics/sendAnalyticsEvent.ts | 10 ++++++++-- .../store-sdk/src/analytics/useAnalyticsEvent.ts | 12 +++++------- packages/store-sdk/src/analytics/wrap.ts | 13 +++++++++---- packages/store-sdk/src/index.ts | 8 ++++++-- 4 files changed, 28 insertions(+), 15 deletions(-) diff --git a/packages/store-sdk/src/analytics/sendAnalyticsEvent.ts b/packages/store-sdk/src/analytics/sendAnalyticsEvent.ts index e8b5be1bec..fed30806c6 100644 --- a/packages/store-sdk/src/analytics/sendAnalyticsEvent.ts +++ b/packages/store-sdk/src/analytics/sendAnalyticsEvent.ts @@ -1,7 +1,13 @@ import { wrap } from './wrap' -import type { AnalyticsEvent } from './wrap' +import type { UnknownEvent, AnalyticsEvent } from './wrap' -export const sendAnalyticsEvent = (event: AnalyticsEvent) => { +export const sendAnalyticsEvent = < + K extends UnknownEvent = AnalyticsEvent, + /** This generic is here so users get the intellisense for event type options from AnalyticsEvent */ + T extends K = K +>( + event: T +) => { try { window.postMessage(wrap(event), window.origin) } catch (e) { diff --git a/packages/store-sdk/src/analytics/useAnalyticsEvent.ts b/packages/store-sdk/src/analytics/useAnalyticsEvent.ts index ead7af0607..3f6b3423c0 100644 --- a/packages/store-sdk/src/analytics/useAnalyticsEvent.ts +++ b/packages/store-sdk/src/analytics/useAnalyticsEvent.ts @@ -1,15 +1,13 @@ import { useCallback, useEffect } from 'react' import { ANALYTICS_EVENT_TYPE, unwrap } from './wrap' -import type { AnalyticsEvent } from './wrap' +import type { UnknownEvent, WrappedAnalyticsEvent } from './wrap' -export type AnalyticsEventHandler = ( - event: AnalyticsEvent -) => void | PromiseLike - -export const useAnalyticsEvent = (handler: AnalyticsEventHandler) => { +export const useAnalyticsEvent = ( + handler: (event: T) => unknown +) => { const callback = useCallback( - (message: MessageEvent) => { + (message: MessageEvent>) => { try { if (message.data.type !== ANALYTICS_EVENT_TYPE) { return diff --git a/packages/store-sdk/src/analytics/wrap.ts b/packages/store-sdk/src/analytics/wrap.ts index 574fa15633..4cbfeef430 100644 --- a/packages/store-sdk/src/analytics/wrap.ts +++ b/packages/store-sdk/src/analytics/wrap.ts @@ -40,14 +40,19 @@ export type AnalyticsEvent = | SignupEvent | ShareEvent -export type WrappedAnalyticsEventData = Omit< +export interface UnknownEvent { + type: string + data: unknown +} + +export type WrappedAnalyticsEventData = Omit< T, 'type' > & { type: `store:${T['type']}` } -export interface WrappedAnalyticsEvent { +export interface WrappedAnalyticsEvent { type: 'AnalyticsEvent' data: WrappedAnalyticsEventData } @@ -55,7 +60,7 @@ export interface WrappedAnalyticsEvent { export const STORE_EVENT_PREFIX = 'store:' export const ANALYTICS_EVENT_TYPE = 'AnalyticsEvent' -export const wrap = ( +export const wrap = ( event: T ): WrappedAnalyticsEvent => ({ @@ -66,7 +71,7 @@ export const wrap = ( }, } as WrappedAnalyticsEvent) -export const unwrap = ( +export const unwrap = ( event: WrappedAnalyticsEvent ): T => { return { diff --git a/packages/store-sdk/src/index.ts b/packages/store-sdk/src/index.ts index a4c2f8226e..b3ef804c9a 100644 --- a/packages/store-sdk/src/index.ts +++ b/packages/store-sdk/src/index.ts @@ -58,10 +58,14 @@ export type { PromotionItem, CurrencyCode, } from './analytics/events/common' -export type { AnalyticsEvent, WrappedAnalyticsEvent } from './analytics/wrap' +export type { + AnalyticsEvent, + WrappedAnalyticsEvent, + WrappedAnalyticsEventData, + UnknownEvent, +} from './analytics/wrap' export { STORE_EVENT_PREFIX, ANALYTICS_EVENT_TYPE } from './analytics/wrap' export { sendAnalyticsEvent } from './analytics/sendAnalyticsEvent' -export type { AnalyticsEventHandler } from './analytics/useAnalyticsEvent' export { useAnalyticsEvent } from './analytics/useAnalyticsEvent' // Faceted Search From 3954ed57c677fc854264291693bf22a7de3df01b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?I=CC=81caro?= Date: Mon, 6 Sep 2021 14:14:36 -0300 Subject: [PATCH 06/14] feat: add docs to analytics --- packages/store-sdk/docs/analytics/README.md | 101 ++++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 packages/store-sdk/docs/analytics/README.md diff --git a/packages/store-sdk/docs/analytics/README.md b/packages/store-sdk/docs/analytics/README.md new file mode 100644 index 0000000000..0465640404 --- /dev/null +++ b/packages/store-sdk/docs/analytics/README.md @@ -0,0 +1,101 @@ +## Cart + +The analytics module lets you manage analytics events based on Google Analytics 4 (GA4) data model. The events are wrapped and sent over standard `postMessage` calls, that shares the event only with the website's origin. The events are received via event listeners. It also supports sending and receiving custom events as the types on the helper functions can be overriden. + +### Sending events + +Analytics events can be sent by using the `sendAnalyticsEvent` function and it's specially useful to send common ecommerce events such as `add_to_cart`. It enforces standard GA4 events via typecheck and intellisense suggestions, but this behavior can be altered via by overriding the function's types. + +To fire a standard GA4 event: +```tsx +import { useCallback } from 'react' +import { sendAnalyticsEvent } from '@vtex/store-sdk' + +const MyComponent = () => { + const addToCartCallback = useCallback(() => { + /* ... */ + + const addToCartEvent = { + type: 'add_to_cart', + data: { + items: [ + /* ... */ + ] + } + } + + sendAnalyticsEvent(addToCartEvent) + }, []) + + return +} +``` + +For custom events, define the type of the event and override the default type via `sendAnalyticsEvent` generics. Your custom event has to have a type and a data field of any kind. + +To fire a custom event: + +```tsx +import { useCallback } from 'react' +import { sendAnalyticsEvent } from '@vtex/store-sdk' + +interface CustomEvent { + type: 'custom_event', + data: { + customProperty?: string + } +} + +const MyComponent = () => { + const customEventCallback = useCallback(() => { + /* ... */ + + const customEvent = { + type: 'custom_event', + data: { + customProperty: 'value' + } + } + + sendAnalyticsEvent(customEvent) + }, []) + + return +} +``` + +### Receiving events + +It's possible to receive analytics events by using the `useAnalyticsEvent` hook. It accepts a handler that will be called everytime an event sent by `sendAnalyticsEvent` arrives. For that reason it can fire both for standard GA4 events and for custom events that a library or a component might be sending. To help users be aware of that possibility, the event received by the handler is, by default, typed as `UnknownEvent`. You can assume it has another type by simply typing the callback function as you wish, but be careful with the unforseen events that might come to this handler. + +To use the `useAnalyticsEvent` hook: + +```tsx +import { useAnalyticsEvent } from '@vtex/store-sdk' +import type { AnalyticsEvent } from '@vtex/store-sdk' + +/** + * Notice that we typed it as AnalyticsEvent, but there may be events that are not from this type. + * + * Since we're dealing with it on a switch and we are providing an empty default clause, + * we're not gonna have issues receiving custom events sent by other components or libraries. + */ +function handler(event: AnalyticsEvent) { + switch(event.type) { + 'add_to_cart': { + /* ... */ + } + + /* ... */ + + default: + } +} + +// In your component: +const MyComponent = () => { + useAnalyticsEvent(handler) + + /* ... */ +} +``` From b37526df18fb8d386106a12ae08e10f54e1c057a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?I=CC=81caro?= Date: Mon, 6 Sep 2021 14:44:54 -0300 Subject: [PATCH 07/14] chore: add test for custom event --- .../test/analytics/sendAnalyticsEvent.test.ts | 50 +++++++++++++++++++ .../test/analytics/useAnalyticsEvent.test.ts | 2 +- 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/packages/store-sdk/test/analytics/sendAnalyticsEvent.test.ts b/packages/store-sdk/test/analytics/sendAnalyticsEvent.test.ts index 8b0c83cfb3..5b4c73e9d7 100644 --- a/packages/store-sdk/test/analytics/sendAnalyticsEvent.test.ts +++ b/packages/store-sdk/test/analytics/sendAnalyticsEvent.test.ts @@ -2,6 +2,33 @@ import { sendAnalyticsEvent } from '../../src/analytics/sendAnalyticsEvent' import type { AddToCartEvent } from '../../src/analytics/events/add_to_cart' import type { WrappedAnalyticsEvent } from '../../src/analytics/wrap' +interface CustomEvent { + type: 'custom_event' + data: { + customDataProperty: string + } + customProperty: string +} + +const eventCustomSample: CustomEvent = { + type: 'custom_event', + data: { + customDataProperty: 'value', + }, + customProperty: 'value', +} + +const wrappedCustomEventSample: WrappedAnalyticsEvent = { + type: 'AnalyticsEvent', + data: { + type: 'store:custom_event', + data: { + customDataProperty: 'value', + }, + customProperty: 'value', + }, +} + const eventSample: AddToCartEvent = { type: 'add_to_cart', data: { @@ -23,6 +50,10 @@ const noop = () => {} const origin = 'http://localhost:8080/' describe('sendAnalyticsEvent', () => { + afterEach(() => { + jest.restoreAllMocks() + }) + it('window.postMessage is called with correct params', () => { const postMessageSpy = jest .spyOn(window, 'postMessage') @@ -38,4 +69,23 @@ describe('sendAnalyticsEvent', () => { expect(postMessageSpy).toHaveBeenCalled() expect(postMessageSpy).toHaveBeenCalledWith(wrappedEventSample, origin) }) + + it('sendAnalyticsEvent is able to send custom events', () => { + const postMessageSpy = jest + .spyOn(window, 'postMessage') + .mockImplementation(noop) + + Object.defineProperty(window, 'origin', { + writable: false, + value: origin, + }) + + sendAnalyticsEvent(eventCustomSample) + + expect(postMessageSpy).toHaveBeenCalled() + expect(postMessageSpy).toHaveBeenCalledWith( + wrappedCustomEventSample, + origin + ) + }) }) diff --git a/packages/store-sdk/test/analytics/useAnalyticsEvent.test.ts b/packages/store-sdk/test/analytics/useAnalyticsEvent.test.ts index 6b9f569dcb..d533edb269 100644 --- a/packages/store-sdk/test/analytics/useAnalyticsEvent.test.ts +++ b/packages/store-sdk/test/analytics/useAnalyticsEvent.test.ts @@ -12,7 +12,7 @@ const eventSample: AddToCartEvent = { } describe('useAnalyticsEvent', () => { - afterAll(() => { + afterEach(() => { jest.restoreAllMocks() }) From a2e2fb37800272362f02a471195672e0451a24d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?I=CC=81caro?= Date: Mon, 6 Sep 2021 15:25:47 -0300 Subject: [PATCH 08/14] docs: adding link to GA4 spec --- packages/store-sdk/docs/analytics/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/store-sdk/docs/analytics/README.md b/packages/store-sdk/docs/analytics/README.md index 0465640404..bfc30a1e9b 100644 --- a/packages/store-sdk/docs/analytics/README.md +++ b/packages/store-sdk/docs/analytics/README.md @@ -1,6 +1,6 @@ ## Cart -The analytics module lets you manage analytics events based on Google Analytics 4 (GA4) data model. The events are wrapped and sent over standard `postMessage` calls, that shares the event only with the website's origin. The events are received via event listeners. It also supports sending and receiving custom events as the types on the helper functions can be overriden. +The analytics module lets you manage analytics events based on [Google Analytics 4 (GA4) data model](https://developers.google.com/analytics/devguides/collection/ga4/reference/events). The events are wrapped and then sent over standard `postMessage` calls, that shares the event only with the website's origin. The events are received via event listeners. It also supports sending and receiving custom events as the types on the helper functions can be overriden. ### Sending events From bfd707bd132e1d4ca6b38545e2cd5eda1e877317 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?I=CC=81caro?= Date: Mon, 6 Sep 2021 15:54:56 -0300 Subject: [PATCH 09/14] chore: removing tsdx incompatible code --- packages/store-sdk/src/analytics/useAnalyticsEvent.ts | 4 ++-- packages/store-sdk/src/analytics/wrap.ts | 6 ++++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/store-sdk/src/analytics/useAnalyticsEvent.ts b/packages/store-sdk/src/analytics/useAnalyticsEvent.ts index 3f6b3423c0..09a3496541 100644 --- a/packages/store-sdk/src/analytics/useAnalyticsEvent.ts +++ b/packages/store-sdk/src/analytics/useAnalyticsEvent.ts @@ -1,13 +1,13 @@ import { useCallback, useEffect } from 'react' import { ANALYTICS_EVENT_TYPE, unwrap } from './wrap' -import type { UnknownEvent, WrappedAnalyticsEvent } from './wrap' +import type { UnknownEvent } from './wrap' export const useAnalyticsEvent = ( handler: (event: T) => unknown ) => { const callback = useCallback( - (message: MessageEvent>) => { + (message: MessageEvent) => { try { if (message.data.type !== ANALYTICS_EVENT_TYPE) { return diff --git a/packages/store-sdk/src/analytics/wrap.ts b/packages/store-sdk/src/analytics/wrap.ts index 4cbfeef430..4347acf09f 100644 --- a/packages/store-sdk/src/analytics/wrap.ts +++ b/packages/store-sdk/src/analytics/wrap.ts @@ -49,7 +49,9 @@ export type WrappedAnalyticsEventData = Omit< T, 'type' > & { - type: `store:${T['type']}` + // Sadly tsdx doesn't support typescript 4.x yet. We should change this type to this when it does: + // type: `store:${T['type']}` + type: string } export interface WrappedAnalyticsEvent { @@ -67,7 +69,7 @@ export const wrap = ( type: ANALYTICS_EVENT_TYPE, data: { ...event, - type: `${STORE_EVENT_PREFIX}${event.type}` as const, + type: `${STORE_EVENT_PREFIX}${event.type}`, }, } as WrappedAnalyticsEvent) From 2a11a4134db7e8e9f765f99f0dbcbace1bfe91be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=8Dcaro=20Azevedo?= Date: Mon, 6 Sep 2021 17:00:13 -0300 Subject: [PATCH 10/14] fix: change title name to analytics --- packages/store-sdk/docs/analytics/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/store-sdk/docs/analytics/README.md b/packages/store-sdk/docs/analytics/README.md index bfc30a1e9b..35284e2f15 100644 --- a/packages/store-sdk/docs/analytics/README.md +++ b/packages/store-sdk/docs/analytics/README.md @@ -1,4 +1,4 @@ -## Cart +## Analytics The analytics module lets you manage analytics events based on [Google Analytics 4 (GA4) data model](https://developers.google.com/analytics/devguides/collection/ga4/reference/events). The events are wrapped and then sent over standard `postMessage` calls, that shares the event only with the website's origin. The events are received via event listeners. It also supports sending and receiving custom events as the types on the helper functions can be overriden. From d8c803b9cff78b411fb08cb1500d42ca4786da5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=8Dcaro=20Azevedo?= Date: Mon, 6 Sep 2021 19:49:11 -0300 Subject: [PATCH 11/14] docs: correct grammar Co-authored-by: Emerson Laurentino --- packages/store-sdk/docs/analytics/README.md | 6 +++--- packages/store-sdk/src/analytics/sendAnalyticsEvent.ts | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/store-sdk/docs/analytics/README.md b/packages/store-sdk/docs/analytics/README.md index 35284e2f15..77271902f0 100644 --- a/packages/store-sdk/docs/analytics/README.md +++ b/packages/store-sdk/docs/analytics/README.md @@ -1,10 +1,10 @@ ## Analytics -The analytics module lets you manage analytics events based on [Google Analytics 4 (GA4) data model](https://developers.google.com/analytics/devguides/collection/ga4/reference/events). The events are wrapped and then sent over standard `postMessage` calls, that shares the event only with the website's origin. The events are received via event listeners. It also supports sending and receiving custom events as the types on the helper functions can be overriden. +The analytics module lets you manage analytics events based on [Google Analytics 4 (GA4) data model](https://developers.google.com/analytics/devguides/collection/ga4/reference/events). The events are wrapped and then sent over standard `postMessage` calls, that share the event only with the website's origin. The events are received via event listeners. It also supports sending and receiving custom events as the types on the helper functions can be overridden. ### Sending events -Analytics events can be sent by using the `sendAnalyticsEvent` function and it's specially useful to send common ecommerce events such as `add_to_cart`. It enforces standard GA4 events via typecheck and intellisense suggestions, but this behavior can be altered via by overriding the function's types. +Analytics events can be sent by using the `sendAnalyticsEvent` function and it's especially useful to send common ecommerce events such as `add_to_cart`. It enforces standard GA4 events via type check and IntelliSense suggestions, but this behavior can be altered via overriding the function's types. To fire a standard GA4 event: ```tsx @@ -66,7 +66,7 @@ const MyComponent = () => { ### Receiving events -It's possible to receive analytics events by using the `useAnalyticsEvent` hook. It accepts a handler that will be called everytime an event sent by `sendAnalyticsEvent` arrives. For that reason it can fire both for standard GA4 events and for custom events that a library or a component might be sending. To help users be aware of that possibility, the event received by the handler is, by default, typed as `UnknownEvent`. You can assume it has another type by simply typing the callback function as you wish, but be careful with the unforseen events that might come to this handler. +It's possible to receive analytics events by using the `useAnalyticsEvent` hook. It accepts a handler that will be called every time an event sent by `sendAnalyticsEvent` arrives. For that reason, it can fire both for standard GA4 events and for custom events that a library or a component might be sending. To help users be aware of that possibility, the event received by the handler is, by default, typed as `UnknownEvent`. You can assume it has another type by simply typing the callback function as you wish, but be careful with the unforeseen events that might come to this handler. To use the `useAnalyticsEvent` hook: diff --git a/packages/store-sdk/src/analytics/sendAnalyticsEvent.ts b/packages/store-sdk/src/analytics/sendAnalyticsEvent.ts index fed30806c6..8e3eba36d6 100644 --- a/packages/store-sdk/src/analytics/sendAnalyticsEvent.ts +++ b/packages/store-sdk/src/analytics/sendAnalyticsEvent.ts @@ -3,7 +3,7 @@ import type { UnknownEvent, AnalyticsEvent } from './wrap' export const sendAnalyticsEvent = < K extends UnknownEvent = AnalyticsEvent, - /** This generic is here so users get the intellisense for event type options from AnalyticsEvent */ + /** This generic is here so users get the IntelliSense for event type options from AnalyticsEvent */ T extends K = K >( event: T From 8e22359247bb4e39eaaedd4058681b264859d72e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=8Dcaro=20Azevedo?= Date: Wed, 8 Sep 2021 11:55:29 -0300 Subject: [PATCH 12/14] docs: improve readability and fix erros on code MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: LarĂ­cia Mota --- packages/store-sdk/docs/analytics/README.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/store-sdk/docs/analytics/README.md b/packages/store-sdk/docs/analytics/README.md index 77271902f0..4ba7cf684f 100644 --- a/packages/store-sdk/docs/analytics/README.md +++ b/packages/store-sdk/docs/analytics/README.md @@ -1,6 +1,6 @@ ## Analytics -The analytics module lets you manage analytics events based on [Google Analytics 4 (GA4) data model](https://developers.google.com/analytics/devguides/collection/ga4/reference/events). The events are wrapped and then sent over standard `postMessage` calls, that share the event only with the website's origin. The events are received via event listeners. It also supports sending and receiving custom events as the types on the helper functions can be overridden. +The analytics module lets you manage analytics events based on [Google Analytics 4 (GA4) data model](https://developers.google.com/analytics/devguides/collection/ga4/reference/events). The events are wrapped and then sent over standard `postMessage` calls, which share the event only with the website's origin. The events are received via event listeners. It also supports sending and receiving custom events as the types on the helper functions can be overridden. ### Sending events @@ -66,7 +66,7 @@ const MyComponent = () => { ### Receiving events -It's possible to receive analytics events by using the `useAnalyticsEvent` hook. It accepts a handler that will be called every time an event sent by `sendAnalyticsEvent` arrives. For that reason, it can fire both for standard GA4 events and for custom events that a library or a component might be sending. To help users be aware of that possibility, the event received by the handler is, by default, typed as `UnknownEvent`. You can assume it has another type by simply typing the callback function as you wish, but be careful with the unforeseen events that might come to this handler. +It's possible to receive analytics events by using the `useAnalyticsEvent` hook. It accepts a handler that will be called every time an event sent by `sendAnalyticsEvent` arrives. For that reason, it can fire both for standard GA4 events and for custom events that a library or a component might be sending. To help users be aware of that possibility, the event received by the handler is, by default, typed as `UnknownEvent`. You can assume it has another type by simply typing the callback function as you wish, but be careful with the unexpected events that might come to this handler. To use the `useAnalyticsEvent` hook: @@ -82,13 +82,15 @@ import type { AnalyticsEvent } from '@vtex/store-sdk' */ function handler(event: AnalyticsEvent) { switch(event.type) { - 'add_to_cart': { + case 'add_to_cart': { /* ... */ } /* ... */ - default: + default: { + /* ... */ + } } } From 5bd3780686449162f86d6627cdc1c8e26c774d9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=8Dcaro=20Azevedo?= Date: Wed, 8 Sep 2021 11:57:28 -0300 Subject: [PATCH 13/14] chore: improve error message from useAnalyticsEvent MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: LarĂ­cia Mota --- packages/store-sdk/src/analytics/useAnalyticsEvent.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/store-sdk/src/analytics/useAnalyticsEvent.ts b/packages/store-sdk/src/analytics/useAnalyticsEvent.ts index 09a3496541..8fd1984888 100644 --- a/packages/store-sdk/src/analytics/useAnalyticsEvent.ts +++ b/packages/store-sdk/src/analytics/useAnalyticsEvent.ts @@ -15,7 +15,7 @@ export const useAnalyticsEvent = ( handler(unwrap(message.data)) } catch (err) { - console.error('Some bad happened while running Analytics handler') + console.error('Something went wrong while running Analytics handler') } }, [handler] From 500dfec77b08b69fd57df271e234ad3e969f9185 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?I=CC=81caro?= Date: Thu, 9 Sep 2021 14:16:09 -0300 Subject: [PATCH 14/14] chore: moving event samples to fixture file --- .../analytics/__fixtures__/EventSamples.ts | 46 +++++++++++++ .../test/analytics/sendAnalyticsEvent.test.ts | 64 ++++--------------- .../test/analytics/useAnalyticsEvent.test.ts | 15 ++--- .../store-sdk/test/analytics/wrap.test.ts | 23 +++---- 4 files changed, 72 insertions(+), 76 deletions(-) create mode 100644 packages/store-sdk/test/analytics/__fixtures__/EventSamples.ts diff --git a/packages/store-sdk/test/analytics/__fixtures__/EventSamples.ts b/packages/store-sdk/test/analytics/__fixtures__/EventSamples.ts new file mode 100644 index 0000000000..5e7bed3785 --- /dev/null +++ b/packages/store-sdk/test/analytics/__fixtures__/EventSamples.ts @@ -0,0 +1,46 @@ +import type { AddToCartEvent } from '../../../src/analytics/events/add_to_cart' +import type { WrappedAnalyticsEvent } from '../../../src/analytics/wrap' + +export interface CustomEvent { + type: 'custom_event' + data: { + customDataProperty: string + } + customProperty: string +} + +export const CUSTOM_EVENT_SAMPLE: CustomEvent = { + type: 'custom_event', + data: { + customDataProperty: 'value', + }, + customProperty: 'value', +} + +export const WRAPPED_CUSTOM_EVENT_SAMPLE: WrappedAnalyticsEvent = { + type: 'AnalyticsEvent', + data: { + type: 'store:custom_event', + data: { + customDataProperty: 'value', + }, + customProperty: 'value', + }, +} + +export const ADD_TO_CART_SAMPLE: AddToCartEvent = { + type: 'add_to_cart', + data: { + items: [{ item_id: 'PRODUCT_ID' }], + }, +} + +export const WRAPPED_ADD_TO_CART_SAMPLE: WrappedAnalyticsEvent = { + type: 'AnalyticsEvent', + data: { + type: 'store:add_to_cart', + data: { + items: [{ item_id: 'PRODUCT_ID' }], + }, + }, +} diff --git a/packages/store-sdk/test/analytics/sendAnalyticsEvent.test.ts b/packages/store-sdk/test/analytics/sendAnalyticsEvent.test.ts index 5b4c73e9d7..e00e2da12c 100644 --- a/packages/store-sdk/test/analytics/sendAnalyticsEvent.test.ts +++ b/packages/store-sdk/test/analytics/sendAnalyticsEvent.test.ts @@ -1,50 +1,11 @@ import { sendAnalyticsEvent } from '../../src/analytics/sendAnalyticsEvent' -import type { AddToCartEvent } from '../../src/analytics/events/add_to_cart' -import type { WrappedAnalyticsEvent } from '../../src/analytics/wrap' - -interface CustomEvent { - type: 'custom_event' - data: { - customDataProperty: string - } - customProperty: string -} - -const eventCustomSample: CustomEvent = { - type: 'custom_event', - data: { - customDataProperty: 'value', - }, - customProperty: 'value', -} - -const wrappedCustomEventSample: WrappedAnalyticsEvent = { - type: 'AnalyticsEvent', - data: { - type: 'store:custom_event', - data: { - customDataProperty: 'value', - }, - customProperty: 'value', - }, -} - -const eventSample: AddToCartEvent = { - type: 'add_to_cart', - data: { - items: [{ item_id: 'PRODUCT_ID' }], - }, -} - -const wrappedEventSample: WrappedAnalyticsEvent = { - type: 'AnalyticsEvent', - data: { - type: 'store:add_to_cart', - data: { - items: [{ item_id: 'PRODUCT_ID' }], - }, - }, -} +import type { CustomEvent } from './__fixtures__/EventSamples' +import { + CUSTOM_EVENT_SAMPLE, + ADD_TO_CART_SAMPLE, + WRAPPED_CUSTOM_EVENT_SAMPLE, + WRAPPED_ADD_TO_CART_SAMPLE, +} from './__fixtures__/EventSamples' const noop = () => {} const origin = 'http://localhost:8080/' @@ -64,10 +25,13 @@ describe('sendAnalyticsEvent', () => { value: origin, }) - sendAnalyticsEvent(eventSample) + sendAnalyticsEvent(ADD_TO_CART_SAMPLE) expect(postMessageSpy).toHaveBeenCalled() - expect(postMessageSpy).toHaveBeenCalledWith(wrappedEventSample, origin) + expect(postMessageSpy).toHaveBeenCalledWith( + WRAPPED_ADD_TO_CART_SAMPLE, + origin + ) }) it('sendAnalyticsEvent is able to send custom events', () => { @@ -80,11 +44,11 @@ describe('sendAnalyticsEvent', () => { value: origin, }) - sendAnalyticsEvent(eventCustomSample) + sendAnalyticsEvent(CUSTOM_EVENT_SAMPLE) expect(postMessageSpy).toHaveBeenCalled() expect(postMessageSpy).toHaveBeenCalledWith( - wrappedCustomEventSample, + WRAPPED_CUSTOM_EVENT_SAMPLE, origin ) }) diff --git a/packages/store-sdk/test/analytics/useAnalyticsEvent.test.ts b/packages/store-sdk/test/analytics/useAnalyticsEvent.test.ts index d533edb269..76b452deaa 100644 --- a/packages/store-sdk/test/analytics/useAnalyticsEvent.test.ts +++ b/packages/store-sdk/test/analytics/useAnalyticsEvent.test.ts @@ -2,14 +2,7 @@ import { renderHook } from '@testing-library/react-hooks' import { useAnalyticsEvent } from '../../src/analytics/useAnalyticsEvent' import { wrap } from '../../src/analytics/wrap' -import type { AddToCartEvent } from '../../src/analytics/events/add_to_cart' - -const eventSample: AddToCartEvent = { - type: 'add_to_cart', - data: { - items: [{ item_id: 'PRODUCT_ID' }], - }, -} +import { ADD_TO_CART_SAMPLE } from './__fixtures__/EventSamples' describe('useAnalyticsEvent', () => { afterEach(() => { @@ -21,14 +14,14 @@ describe('useAnalyticsEvent', () => { jest.spyOn(window, 'addEventListener').mockImplementation((_, fn) => { if (typeof fn === 'function') { - fn(new MessageEvent('message', { data: wrap(eventSample) })) + fn(new MessageEvent('message', { data: wrap(ADD_TO_CART_SAMPLE) })) } }) renderHook(() => useAnalyticsEvent(handler)) expect(handler).toHaveBeenCalled() - expect(handler).toHaveBeenCalledWith(eventSample) + expect(handler).toHaveBeenCalledWith(ADD_TO_CART_SAMPLE) }) it('useAnalyticsEvent ignores events that are not AnalyticsEvent', async () => { @@ -38,7 +31,7 @@ describe('useAnalyticsEvent', () => { if (typeof fn === 'function') { fn( new MessageEvent('message', { - data: { ...wrap(eventSample), type: 'OtherEventType' }, + data: { ...wrap(ADD_TO_CART_SAMPLE), type: 'OtherEventType' }, }) ) } diff --git a/packages/store-sdk/test/analytics/wrap.test.ts b/packages/store-sdk/test/analytics/wrap.test.ts index 091d02ed3b..a67945042a 100644 --- a/packages/store-sdk/test/analytics/wrap.test.ts +++ b/packages/store-sdk/test/analytics/wrap.test.ts @@ -1,40 +1,33 @@ import { wrap, unwrap } from '../../src/analytics/wrap' -import type { AddToCartEvent } from '../../src/analytics/events/add_to_cart' - -const eventSample: AddToCartEvent = { - type: 'add_to_cart', - data: { - items: [{ item_id: 'PRODUCT_ID' }], - }, -} +import { ADD_TO_CART_SAMPLE } from './__fixtures__/EventSamples' describe('wrap and unwrap functions', () => { it('wrap function wraps event with AnalyticsEvent type', () => { - const { type } = wrap(eventSample) + const { type } = wrap(ADD_TO_CART_SAMPLE) expect(type).toBe('AnalyticsEvent') }) it('wrap function prefixes the event type', () => { - const wrappedEvent = wrap(eventSample) + const wrappedEvent = wrap(ADD_TO_CART_SAMPLE) const { type: wrappedEventType } = wrappedEvent.data - const { type: originalType } = eventSample + const { type: originalType } = ADD_TO_CART_SAMPLE expect(wrappedEventType).toBe(`store:${originalType}`) }) it('wrap function preserves all event data but the type', () => { - const wrappedEvent = wrap(eventSample) + const wrappedEvent = wrap(ADD_TO_CART_SAMPLE) const { type: _, ...wrappedEventRest } = wrappedEvent.data - const { type: __, ...originalEventRest } = eventSample + const { type: __, ...originalEventRest } = ADD_TO_CART_SAMPLE expect(wrappedEventRest).toStrictEqual(originalEventRest) }) it('unwrap function preserves the original event', () => { - const wrappedEvent = wrap(eventSample) + const wrappedEvent = wrap(ADD_TO_CART_SAMPLE) const unwrappedEvent = unwrap(wrappedEvent) ?? {} - expect(unwrappedEvent).toStrictEqual(eventSample) + expect(unwrappedEvent).toStrictEqual(ADD_TO_CART_SAMPLE) }) })