Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,13 @@ scaleway-lib is a set of NPM packages used at Scaleway.
![npm bundle size](https://img.shields.io/bundlephobia/min/@scaleway/use-segment)
![npm](https://img.shields.io/npm/v/@scaleway/use-segment)

- [`@scaleway/use-gtm`](./packages/use-gtm/README.md):
A tiny hook to handle gtm.

![npm](https://img.shields.io/npm/dm/@scaleway/use-gtm)
![npm bundle size](https://img.shields.io/bundlephobia/min/@scaleway/use-gtm)
![npm](https://img.shields.io/npm/v/@scaleway/use-gtm)

- [`@scaleway/use-i18n`](./packages/use-i18n/README.md):
A tiny hook to handle i18n.

Expand Down
10 changes: 10 additions & 0 deletions packages/use-gtm/.eslintrc.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
const { join } = require('path')

module.exports = {
rules: {
'import/no-extraneous-dependencies': [
'error',
{ packageDir: [__dirname, join(__dirname, '../../')] },
],
},
}
5 changes: 5 additions & 0 deletions packages/use-gtm/.npmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
**/__tests__/**
examples/
src
.eslintrc.cjs
!.npmignore
82 changes: 82 additions & 0 deletions packages/use-gtm/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# `@scaleway/use-gtm`

## A tiny provider to handle Google Tag Manager in React

## Install

```bash
$ pnpm add @scaleway/use-gtm
```

## Usage

### Basic

```tsx
import GTMProvider, { useGTM } from '@scaleway/use-gtm'

const Page = () => {
const { sendGTM } = useGTM()

sendGTM?.({
hello: 'world
})

return <p>Hello World</p>
}

const App = () => (
<GTMProvider id="testId">
<Page />
</GTMProvider>
)
```

### With injected events

```tsx
import GTMProvider, { useGTM } from '@scaleway/use-gtm'

const events = {
sampleEvent: (sendGTM?: SendGTM) => (message: string) => {
sendGTM?.({
event: 'sampleEvent',
hello: message,
})
}
}

const Page = () => {
const { events } = useGTM()

events.sampleEvent?.('world')

return <p>Hello World</p>
}

const App = () => (
<GTMProvider id="testId">
<Page />
</GTMProvider>
)
```

### With global setter

```tsx
import GTMProvider, { sendGTM } from '@scaleway/use-gtm'

const Page = () => {
sendGTM?.({
hello: 'world
})

return <p>Hello World</p>
}

const App = () => (
<GTMProvider id="testId">
<Page />
</GTMProvider>
)
```
32 changes: 32 additions & 0 deletions packages/use-gtm/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"name": "@scaleway/use-gtm",
"version": "1.0.0",
"description": "A small hook to handle gtm in a react app",
"keywords": [
"react",
"reactjs",
"hooks",
"google",
"google tag manager",
"gtm"
],
"type": "module",
"main": "dist/index.js",
"module": "dist/index.js",
"types": "dist/index.d.ts",
"browser": {
"dist/index.js": "./dist/index.browser.js"
},
"publishConfig": {
"access": "public"
},
"repository": {
"type": "git",
"url": "https://github.com/scaleway/scaleway-lib",
"directory": "packages/use-gtm"
},
"license": "MIT",
"peerDependencies": {
"react": "17.x"
}
}
59 changes: 59 additions & 0 deletions packages/use-gtm/src/__tests__/__snapshots__/index.tsx.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`GTM hook Provider should call onLoadError if script fail to load 1`] = `
"<script src=\\"https://www.googletagmanager.com/gtm.js?id=testId\\"></script><script>window.dataLayer = window.dataLayer || [];</script><script>(function(w,d,s,l,i){w[l]=w[l]||[];
w[l].push({'gtm.start': new Date().getTime(),event:'gtm.js'});
var f=d.getElementsByTagName(s)[0],j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';
j.async=true;j.src='https://www.googletagmanager.com/gtm.js?id='+i+dl+'';

j.addEventListener('error', function() {
var _ge = new CustomEvent('gtm_loading_error', { bubbles: true });
d.dispatchEvent(_ge);
});

f.parentNode.insertBefore(j,f);
})(window,document,'script','dataLayer','testId');</script>"
`;

exports[`GTM hook Provider should load when id and environment is provided 1`] = `
"<script src=\\"https://www.googletagmanager.com/gtm.js?id=testId&amp;gtm_auth=gtm&amp;gtm_preview=world&amp;gtm_cookies_win=x\\"></script><script>window.dataLayer = window.dataLayer || [];</script><script>(function(w,d,s,l,i){w[l]=w[l]||[];
w[l].push({'gtm.start': new Date().getTime(),event:'gtm.js'});
var f=d.getElementsByTagName(s)[0],j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';
j.async=true;j.src='https://www.googletagmanager.com/gtm.js?id='+i+dl+'&gtm_auth=gtm&gtm_preview=world&gtm_cookies_win=x';

j.addEventListener('error', function() {
var _ge = new CustomEvent('gtm_loading_error', { bubbles: true });
d.dispatchEvent(_ge);
});

f.parentNode.insertBefore(j,f);
})(window,document,'script','dataLayer','testId');</script>"
`;

exports[`GTM hook Provider should load when id is provided 1`] = `
"<script src=\\"https://www.googletagmanager.com/gtm.js?id=testId\\"></script><script>window.dataLayer = window.dataLayer || [];</script><script>(function(w,d,s,l,i){w[l]=w[l]||[];
w[l].push({'gtm.start': new Date().getTime(),event:'gtm.js'});
var f=d.getElementsByTagName(s)[0],j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';
j.async=true;j.src='https://www.googletagmanager.com/gtm.js?id='+i+dl+'';

j.addEventListener('error', function() {
var _ge = new CustomEvent('gtm_loading_error', { bubbles: true });
d.dispatchEvent(_ge);
});

f.parentNode.insertBefore(j,f);
})(window,document,'script','dataLayer','testId');</script>"
`;

exports[`GTM hook Provider should load with events when provided 1`] = `
Array [
Object {
"event": "gtm.js",
"gtm.start": 1618272000000,
},
Object {
"event": "sampleEvent",
"extra": "test",
},
]
`;
121 changes: 121 additions & 0 deletions packages/use-gtm/src/__tests__/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import { fireEvent } from '@testing-library/react'
import { renderHook } from '@testing-library/react-hooks'
import mockdate from 'mockdate'
import { ReactNode } from 'react'
import GTMProvider, { SendGTM, useGTM } from '..'
import { GTMProviderProps } from '../useGTM'

const defaultEvents = {
sampleEvent: (sendGTM?: SendGTM) => (extraValue: string) => {
sendGTM?.({
event: 'sampleEvent',
extra: extraValue,
})
},
}

type DefaultEvents = typeof defaultEvents

const wrapper =
({
id,
events,
environment,
onLoadError,
}: Omit<GTMProviderProps<DefaultEvents>, 'children'>) =>
({ children }: { children: ReactNode }) =>
(
<GTMProvider
id={id}
events={events}
environment={environment}
onLoadError={onLoadError}
>
{children}
</GTMProvider>
)

describe('GTM hook', () => {
beforeEach(() => {
mockdate.set('4/13/2021')
})

afterEach(() => {
mockdate.reset()
document.head.innerHTML = ''
window.dataLayer = undefined
jest.restoreAllMocks()
})

it('useGTM should not be defined without GTMProvider', () => {
const { result } = renderHook(() => useGTM())
expect(() => {
expect(result.current).toBe(undefined)
}).toThrow(Error('useGTM must be used within a GTMProvider'))
})

it('Provider should call onLoadError if script fail to load', () => {
renderHook(() => useGTM<DefaultEvents>(), {
wrapper: wrapper({
id: 'testId',
}),
})

expect(document.head.innerHTML).toMatchSnapshot()
})

it('Provider should load when id is provided', () => {
renderHook(() => useGTM<DefaultEvents>(), {
wrapper: wrapper({
id: 'testId',
}),
})

expect(document.head.innerHTML).toMatchSnapshot()
})

it('Provider should load when id and environment is provided', () => {
renderHook(() => useGTM<DefaultEvents>(), {
wrapper: wrapper({
environment: {
auth: 'gtm',
preview: 'world',
},
id: 'testId',
}),
})

expect(document.head.innerHTML).toMatchSnapshot()
})

it('Provider should load with events when provided', () => {
const { result } = renderHook(() => useGTM<DefaultEvents>(), {
wrapper: wrapper({
events: defaultEvents,
id: 'testId',
}),
})

expect(result.current.events.sampleEvent('test')).toBe(undefined)
expect(window.dataLayer).toMatchSnapshot()
// @ts-expect-error if type infering works this should be an error
expect(result.current.events.sampleEvent()).toBe(undefined)
})

it('Provider should load onLoadError when script fail to load', () => {
const onLoadError = jest.fn()

renderHook(() => useGTM<DefaultEvents>(), {
wrapper: wrapper({
id: 'testId',
onLoadError,
}),
})

const script = document.querySelector(
`script[src="https://www.googletagmanager.com/gtm.js?id=testId"]`,
) as Element
fireEvent.error(script)
expect(onLoadError).toHaveBeenCalledTimes(1)
})
})
6 changes: 6 additions & 0 deletions packages/use-gtm/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import GTMProvider from './useGTM'

export { useGTM, sendGTM } from './useGTM'
export type { SendGTM } from './types'

export default GTMProvider
56 changes: 56 additions & 0 deletions packages/use-gtm/src/scripts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { GTMEnvironment } from './types'

export const DATALAYER_NAME = 'dataLayer'
export const LOAD_ERROR_EVENT = 'gtm_loading_error'

const flattenEnvironment = (environment?: GTMEnvironment) =>
environment
? `&${Object.entries(environment)
.filter(([, value]) => !!value)
.map(([key, value]) => `gtm_${key}=${value}`, '')
.join('&')}&gtm_cookies_win=x`
: ''

const generateSnippets = (id: string, environment?: GTMEnvironment) => {
const env = flattenEnvironment(environment)

return {
dataLayerInit: `window.${DATALAYER_NAME} = window.${DATALAYER_NAME} || [];`,
noScript: `<iframe src="https://www.googletagmanager.com/ns.html?id=${id}${env}" height="0" width="0" style="display:none;visibility:hidden" id="tag-manager"></iframe>`,
script: `(function(w,d,s,l,i){w[l]=w[l]||[];
w[l].push({'gtm.start': new Date().getTime(),event:'gtm.js'});
var f=d.getElementsByTagName(s)[0],j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';
j.async=true;j.src='https://www.googletagmanager.com/gtm.js?id='+i+dl+'${env}';

j.addEventListener('error', function() {
var _ge = new CustomEvent('${LOAD_ERROR_EVENT}', { bubbles: true });
d.dispatchEvent(_ge);
});

f.parentNode.insertBefore(j,f);
})(window,document,'script','${DATALAYER_NAME}','${id}');`,
}
}

const generateScripts = (id: string, environment?: GTMEnvironment) => {
const {
dataLayerInit: dataLayerInitSnippet,
noScript: noScriptSnippet,
script: scriptSnippet,
} = generateSnippets(id, environment)

const dataLayerInit = document.createElement('script')
dataLayerInit.innerHTML = dataLayerInitSnippet
const noScript = document.createElement('noscript')
noScript.innerHTML = noScriptSnippet
const script = document.createElement('script')
script.innerHTML = scriptSnippet

return {
dataLayerInit,
noScript,
script,
}
}

export default generateScripts
Loading