diff --git a/jest.config.js b/jest.config.js index 52a5d31763..89598538cc 100644 --- a/jest.config.js +++ b/jest.config.js @@ -26,5 +26,6 @@ module.exports = { '/nx-dev/data-access-documents', '/nx-dev/data-access-menu', '/nx-dev/feature-search', + '/nx-dev/feature-analytics', ], }; diff --git a/nx-dev/feature-analytics/.babelrc b/nx-dev/feature-analytics/.babelrc new file mode 100644 index 0000000000..ccae900be4 --- /dev/null +++ b/nx-dev/feature-analytics/.babelrc @@ -0,0 +1,12 @@ +{ + "presets": [ + [ + "@nrwl/react/babel", + { + "runtime": "automatic", + "useBuiltIns": "usage" + } + ] + ], + "plugins": [] +} diff --git a/nx-dev/feature-analytics/.eslintrc.json b/nx-dev/feature-analytics/.eslintrc.json new file mode 100644 index 0000000000..a3ae45e7f1 --- /dev/null +++ b/nx-dev/feature-analytics/.eslintrc.json @@ -0,0 +1,21 @@ +{ + "extends": ["plugin:@nrwl/nx/react", "../../.eslintrc.json"], + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], + "parserOptions": { + "project": ["nx-dev/feature-analytics/tsconfig.*?.json"] + }, + "rules": {} + }, + { + "files": ["*.ts", "*.tsx"], + "rules": {} + }, + { + "files": ["*.js", "*.jsx"], + "rules": {} + } + ] +} diff --git a/nx-dev/feature-analytics/README.md b/nx-dev/feature-analytics/README.md new file mode 100644 index 0000000000..fac268ef68 --- /dev/null +++ b/nx-dev/feature-analytics/README.md @@ -0,0 +1,7 @@ +# nx-dev-feature-analytics + +This library was generated with [Nx](https://nx.dev). + +## Running unit tests + +Run `nx test nx-dev-feature-analytics` to execute the unit tests via [Jest](https://jestjs.io). diff --git a/nx-dev/feature-analytics/jest.config.js b/nx-dev/feature-analytics/jest.config.js new file mode 100644 index 0000000000..92ff04874f --- /dev/null +++ b/nx-dev/feature-analytics/jest.config.js @@ -0,0 +1,9 @@ +module.exports = { + displayName: 'nx-dev-feature-analytics', + preset: '../../jest.preset.js', + transform: { + '^.+\\.[tj]sx?$': 'babel-jest', + }, + moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'], + coverageDirectory: '../../coverage/nx-dev/feature-analytics', +}; diff --git a/nx-dev/feature-analytics/src/index.ts b/nx-dev/feature-analytics/src/index.ts new file mode 100644 index 0000000000..b1b2eaad3a --- /dev/null +++ b/nx-dev/feature-analytics/src/index.ts @@ -0,0 +1 @@ +export * from './lib/google-analytics'; diff --git a/nx-dev/feature-analytics/src/lib/google-analytics.ts b/nx-dev/feature-analytics/src/lib/google-analytics.ts new file mode 100644 index 0000000000..01362efa6e --- /dev/null +++ b/nx-dev/feature-analytics/src/lib/google-analytics.ts @@ -0,0 +1,40 @@ +/** + * [Gtag sending data documentation](https://developers.google.com/analytics/devguides/collection/gtagjs/sending-data) + * + * [About Events](https://support.google.com/analytics/answer/1033068/about-events) + */ +import { Gtag } from './gtag'; + +declare const gtag: Gtag; + +export function sendPageViewEvent(data: { + gaId: string; + path?: string; + title?: string; +}): void { + try { + gtag('config', data.gaId, { + ...(!!data.path && { page_path: data.path }), + ...(!!data.title && { page_title: data.title }), + }); + } catch (exception) { + throw new Error(`Cannot send Google Tag event: ${exception}`); + } +} + +export function sendCustomEvent( + action: string, + category: string, + label: string, + value?: number +): void { + try { + gtag('event', action, { + event_category: category, + event_label: label, + value, + }); + } catch (error) { + throw new Error(`Cannot send Google Tag event: ${error}`); + } +} diff --git a/nx-dev/feature-analytics/src/lib/gtag.ts b/nx-dev/feature-analytics/src/lib/gtag.ts new file mode 100644 index 0000000000..8b23431101 --- /dev/null +++ b/nx-dev/feature-analytics/src/lib/gtag.ts @@ -0,0 +1,123 @@ +export interface Gtag { + ( + command: 'config', + targetId: string, + config?: ConfigParams | ControlParams | EventParams | CustomParams + ): void; + (command: 'set', targetId: string, config: CustomParams | boolean): void; + (command: 'set', config: CustomParams): void; + (command: 'js', config: Date): void; + ( + command: 'event', + eventName: EventNames | string, + eventParams?: ControlParams | EventParams | CustomParams + ): void; + ( + command: 'get', + targetId: string, + fieldName: FieldNames | string, + callback?: (field: string) => any + ): void; + ( + command: 'consent', + consentArg: ConsentArg | string, + consentParams: ConsentParams + ): void; +} + +interface CustomParams { + [key: string]: any; +} + +interface ConfigParams { + page_location?: string; + page_path?: string; + page_title?: string; + send_page_view?: boolean; +} + +interface ControlParams { + groups?: string | string[]; + send_to?: string | string[]; + event_callback?: () => void; + event_timeout?: number; +} + +type EventNames = + | 'add_payment_info' + | 'add_to_cart' + | 'add_to_wishlist' + | 'begin_checkout' + | 'checkout_progress' + | 'exception' + | 'generate_lead' + | 'login' + | 'page_view' + | 'purchase' + | 'refund' + | 'remove_from_cart' + | 'screen_view' + | 'search' + | 'select_content' + | 'set_checkout_option' + | 'share' + | 'sign_up' + | 'timing_complete' + | 'view_item' + | 'view_item_list' + | 'view_promotion' + | 'view_search_results'; + +interface EventParams { + checkout_option?: string; + checkout_step?: number; + content_id?: string; + content_type?: string; + coupon?: string; + currency?: string; + description?: string; + fatal?: boolean; + items?: Item[]; + method?: string; + number?: string; + promotions?: Promotion[]; + screen_name?: string; + search_term?: string; + shipping?: Currency; + tax?: Currency; + transaction_id?: string; + value?: number; + event_label?: string; + event_category?: string; +} + +type Currency = string | number; + +interface Item { + brand?: string; + category?: string; + creative_name?: string; + creative_slot?: string; + id?: string; + location_id?: string; + name?: string; + price?: Currency; + quantity?: number; +} + +interface Promotion { + creative_name?: string; + creative_slot?: string; + id?: string; + name?: string; +} + +type FieldNames = 'client_id' | 'session_id' | 'gclid'; + +type ConsentArg = 'default' | 'update'; +interface ConsentParams { + ad_storage?: 'granted' | 'denied'; + analytics_storage?: 'granted' | 'denied'; + wait_for_update?: number; + region?: string[]; +} diff --git a/nx-dev/feature-analytics/tsconfig.json b/nx-dev/feature-analytics/tsconfig.json new file mode 100644 index 0000000000..37ab84bcdb --- /dev/null +++ b/nx-dev/feature-analytics/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "jsx": "react-jsx", + "allowJs": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ] +} diff --git a/nx-dev/feature-analytics/tsconfig.lib.json b/nx-dev/feature-analytics/tsconfig.lib.json new file mode 100644 index 0000000000..71adee65df --- /dev/null +++ b/nx-dev/feature-analytics/tsconfig.lib.json @@ -0,0 +1,13 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "types": ["node"] + }, + "files": [ + "../../node_modules/@nrwl/react/typings/cssmodule.d.ts", + "../../node_modules/@nrwl/react/typings/image.d.ts" + ], + "exclude": ["**/*.spec.ts", "**/*.spec.tsx"], + "include": ["**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx"] +} diff --git a/nx-dev/feature-analytics/tsconfig.spec.json b/nx-dev/feature-analytics/tsconfig.spec.json new file mode 100644 index 0000000000..559410b96a --- /dev/null +++ b/nx-dev/feature-analytics/tsconfig.spec.json @@ -0,0 +1,15 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "module": "commonjs", + "types": ["jest", "node"] + }, + "include": [ + "**/*.spec.ts", + "**/*.spec.tsx", + "**/*.spec.js", + "**/*.spec.jsx", + "**/*.d.ts" + ] +} diff --git a/nx-dev/feature-doc-viewer/src/lib/code-block.tsx b/nx-dev/feature-doc-viewer/src/lib/code-block.tsx index aef6144dd8..e9fd0bd92b 100644 --- a/nx-dev/feature-doc-viewer/src/lib/code-block.tsx +++ b/nx-dev/feature-doc-viewer/src/lib/code-block.tsx @@ -5,11 +5,13 @@ import { CopyToClipboard } from 'react-copy-to-clipboard'; export function CodeBlock({ text, language, + callback, ...rest }: { text: string; language: string; [key: string]: any; + callback: (text: string) => void; }) { const [copied, setCopied] = useState(false); useEffect(() => { @@ -25,7 +27,13 @@ export function CodeBlock({ }, [copied]); return (
- setCopied(true)}> + { + setCopied(true); + callback(text); + }} + >
); diff --git a/nx-dev/feature-search/.eslintrc.json b/nx-dev/feature-search/.eslintrc.json index abfab9984c..5237b5d6ab 100644 --- a/nx-dev/feature-search/.eslintrc.json +++ b/nx-dev/feature-search/.eslintrc.json @@ -5,7 +5,7 @@ { "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], "parserOptions": { - "project": ["/nx-dev/feature-search/tsconfig.*?.json"] + "project": ["nx-dev/feature-search/tsconfig.*?.json"] }, "rules": {} }, diff --git a/nx-dev/feature-search/src/lib/algolia-search.tsx b/nx-dev/feature-search/src/lib/algolia-search.tsx index dc7a0afafd..5371cec2bb 100644 --- a/nx-dev/feature-search/src/lib/algolia-search.tsx +++ b/nx-dev/feature-search/src/lib/algolia-search.tsx @@ -1,4 +1,4 @@ -import { useState, useCallback, useRef, useEffect } from 'react'; +import { useCallback, useEffect, useRef, useState } from 'react'; import { createPortal } from 'react-dom'; import Link from 'next/link'; import Head from 'next/head'; diff --git a/nx-dev/nx-dev/pages/_app.tsx b/nx-dev/nx-dev/pages/_app.tsx index 47fc42d52c..0dfb7e1ccd 100644 --- a/nx-dev/nx-dev/pages/_app.tsx +++ b/nx-dev/nx-dev/pages/_app.tsx @@ -1,9 +1,19 @@ -import React from 'react'; +import React, { useEffect } from 'react'; import { AppProps } from 'next/app'; import Head from 'next/head'; +import { useRouter } from 'next/router'; +import { sendPageViewEvent } from '@nrwl/nx-dev/feature-analytics'; import '../styles/main.css'; export default function CustomApp({ Component, pageProps }: AppProps) { + const router = useRouter(); + const gaMeasurementId = 'UA-88380372-10'; + useEffect(() => { + const handleRouteChange = (url: URL) => + sendPageViewEvent(gaMeasurementId, { path: url }); + router.events.on('routeChangeStart', (url) => handleRouteChange(url)); + return () => router.events.off('routeChangeStart', handleRouteChange); + }, [router]); return ( <> @@ -34,6 +44,23 @@ export default function CustomApp({ Component, pageProps }: AppProps) { + {/* Global Site Tag (gtag.js) - Google Analytics */} +