From a0de92deadc8c4afa6cc01b20ecba62fbff21dfd Mon Sep 17 00:00:00 2001 From: lucent1090 Date: Wed, 23 Jun 2021 14:52:59 +0200 Subject: [PATCH] feat: add plugin for flushing datalayer event (#78) * feat: wIP: add plugin for flushing data layer properties * feat: wIP - exporting the flushing data layer properties plugin * test(plugins/getflushedpayload/*): add tests to getflushedpayload Add unit test for getFlushedPayload with small refactoring of function code * chore: move getFlushedPayload to top-level export * docs(readme): fix wrong example code import getFlushedPayload from top level Co-authored-by: wilfernandesjr --- README.md | 31 +++++++ src/index.ts | 4 +- .../getFlushedPayload.spec.ts | 90 +++++++++++++++++++ .../getFlushedPayload/getFlushedPayload.ts | 63 +++++++++++++ src/plugins/getFlushedPayload/index.ts | 18 ++++ 5 files changed, 205 insertions(+), 1 deletion(-) create mode 100644 src/plugins/getFlushedPayload/getFlushedPayload.spec.ts create mode 100644 src/plugins/getFlushedPayload/getFlushedPayload.ts create mode 100644 src/plugins/getFlushedPayload/index.ts diff --git a/README.md b/README.md index 9731bfd..f034c06 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,8 @@ Collector is a library of React components and hooks that facilitates contextual - [useClickTrigger](#useclicktrigger) - [useSubmitTrigger](#usesubmittrigger) - [usePageViewTrigger](#usepageviewtrigger) +- [Plugin](#plugin) + - [getFlushedPayload](#getflushedpayload) - [Code of Conduct (CoC)](#code-of-conduct-coc) - [Maintainers](#maintainers) - [About SumUp](#about-sumup) @@ -373,6 +375,35 @@ function PageActive({ location, children }: Props) { } ``` +## Plugin + +Helpers for specific issue. + +### getFlushedPayLoad + +If you are using Google Tag Manager(GTM) as your dispatch consumer, there is a known behaviour that GTM persists variables until they got flushed. For a non-nested event, a fixed schema with default undefined value flushes unused variable thus they don't pollute states for the next event. For a designed nested variable, eg, `customParameters` in Collector, a nested flush helps to keep states clean. In this plugin, an aggregated custom parameters based on payload history will be set as undefined and flushed by GTM. + +You can find an example code here. + +```jsx +import React from 'react'; +import { getFlushedPayload } from '@sumup/collector'; + +function App() { + const handleDispatch = React.useCallback((event) => { + // getFlushedPayload return a new event with flushed payload + const flushedEvent = getFlushedPayload(window.dataLayer, event); + window.dataLayer.push(flushedEvent) + }, []); + + return ( + + ... + + ); +} +``` + ## Code of Conduct (CoC) We want to foster an inclusive and friendly community around our Open Source efforts. Like all SumUp Open Source projects, this project follows the Contributor Covenant Code of Conduct. Please, [read it and follow it](CODE_OF_CONDUCT.md). diff --git a/src/index.ts b/src/index.ts index f4ee00f..c352c61 100644 --- a/src/index.ts +++ b/src/index.ts @@ -20,6 +20,7 @@ import useClickTrigger from './hooks/useClickTrigger'; import useSubmitTrigger from './hooks/useSubmitTrigger'; import usePageViewTrigger from './hooks/usePageViewTrigger'; import usePageActiveTrigger from './hooks/usePageActiveTrigger'; +import getFlushedPayload from './plugins/getFlushedPayload'; import * as Types from './types'; export { @@ -29,7 +30,8 @@ export { useClickTrigger, useSubmitTrigger, usePageViewTrigger, - usePageActiveTrigger + usePageActiveTrigger, + getFlushedPayload }; export type Dispatch = Types.Dispatch; diff --git a/src/plugins/getFlushedPayload/getFlushedPayload.spec.ts b/src/plugins/getFlushedPayload/getFlushedPayload.spec.ts new file mode 100644 index 0000000..959398b --- /dev/null +++ b/src/plugins/getFlushedPayload/getFlushedPayload.spec.ts @@ -0,0 +1,90 @@ +/** + * Copyright 2020, SumUp Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Events, Payload } from '../../types'; + +import getFlushedPayload from './getFlushedPayload'; + +const baseEvent1: Payload = { + app: 'app', + view: 'view', + elementTree: [], + event: Events.click, + timestamp: 1, + customParameters: { A: 'A', B: true, C: [1, 2, 3] } +}; + +const baseEvent2: Payload = { + app: 'app', + view: 'view', + elementTree: [], + event: Events.click, + timestamp: 2, + customParameters: { D: { A: 'D' } } +}; + +describe('getFlushedPayload', () => { + it('should set unused custom parameters to undefined based on payloads history', () => { + const previousPayloads = [baseEvent1, baseEvent2]; + const payload = { + ...baseEvent1, + customParameters: { B: false, C: 'string' } + }; + const result = getFlushedPayload(previousPayloads, payload); + expect(result).toStrictEqual({ + ...baseEvent1, + _clear: true, + customParameters: { A: undefined, B: false, C: 'string', D: undefined } + }); + }); + + it('should be an aggregated object when no new custom parameters present', () => { + const previousPayloads = [baseEvent1, baseEvent2]; + const payload = { + app: 'app', + view: 'view', + elementTree: [], + event: Events.click, + timestamp: 3 + }; + const result = getFlushedPayload(previousPayloads, payload); + expect(result).toStrictEqual({ + ...payload, + _clear: true, + customParameters: { + A: undefined, + B: undefined, + C: undefined, + D: undefined + } + }); + }); + + it('should be new payload when no previous custom paramaters is found', () => { + const payload = { + app: 'app', + view: 'view', + elementTree: [], + event: Events.click, + timestamp: 4 + }; + const previousPayloads = [payload]; + const result = getFlushedPayload(previousPayloads, baseEvent1); + expect(result).toStrictEqual({ + ...baseEvent1, + _clear: true + }); + }); +}); diff --git a/src/plugins/getFlushedPayload/getFlushedPayload.ts b/src/plugins/getFlushedPayload/getFlushedPayload.ts new file mode 100644 index 0000000..4a9d09b --- /dev/null +++ b/src/plugins/getFlushedPayload/getFlushedPayload.ts @@ -0,0 +1,63 @@ +/** + * Copyright 2021, SumUp Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Payload } from '../../types'; + +/** + * Reference: + * https://www.simoahava.com/gtm-tips/remember-to-flush-unused-data-layer-variables/ + * + * GetFlushedPayload will aggregate custom parameters based on previous payload history + * and set all previous value to undefined. By setting to undefined, GTM will flush the + * variable and not polluting next states of datalayer. + * @param previousPayloads payload history + * @param payload new payload + */ +const getFlushedPayload = ( + previousPayloads: Payload[], + payload: Payload +): Payload => { + const aggregatedParameters = previousPayloads.reduce((accu, current) => { + const customParams = current.customParameters || {}; + return { ...accu, ...customParams }; + }, {}); + + // reset values to undefined in an object + const resettedParameters = Object.keys(aggregatedParameters).reduce( + (accu, key) => ({ + ...accu, + [key]: undefined + }), + {} + ); + + const flushedPayload = { + ...payload, + customParameters: { + ...resettedParameters, + ...payload.customParameters + }, + /** + * GTM property to prevent recursive merge of nested object/array + * inside customParameters. + * Ref: https://www.simoahava.com/analytics/two-simple-data-model-tricks/ + * */ + _clear: true + }; + + return flushedPayload; +}; + +export default getFlushedPayload; diff --git a/src/plugins/getFlushedPayload/index.ts b/src/plugins/getFlushedPayload/index.ts new file mode 100644 index 0000000..d6daaa4 --- /dev/null +++ b/src/plugins/getFlushedPayload/index.ts @@ -0,0 +1,18 @@ +/** + * Copyright 2020, SumUp Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import getFlushedPayload from './getFlushedPayload'; + +export default getFlushedPayload;