├── .prettierignore ├── jest-setup.ts ├── src ├── lib │ ├── types │ │ ├── router.ts │ │ ├── ym.ts │ │ ├── options.ts │ │ ├── parameters.ts │ │ └── events.ts │ ├── ym.ts │ └── __tests__ │ │ └── ym.test.ts ├── index.ts ├── hooks │ ├── useTrackRouteChange.ts │ ├── useMetrica.ts │ └── __tests__ │ │ ├── useTrackRouteChange.test.ts │ │ └── useMetrica.test.tsx └── components │ ├── YandexMetricaProvider.tsx │ └── __tests__ │ └── YandexMetricaProvider.test.tsx ├── .vscode └── settings.json ├── .prettierrc.json ├── .gitignore ├── jest.config.ts ├── tsconfig.json ├── .github └── workflows │ ├── publish.yml │ └── check.yml ├── .eslintrc.json ├── LICENSE ├── package.json ├── README.md └── pnpm-lock.yaml /.prettierignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | pnpm-lock.yaml 3 | -------------------------------------------------------------------------------- /jest-setup.ts: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom'; 2 | -------------------------------------------------------------------------------- /src/lib/types/router.ts: -------------------------------------------------------------------------------- 1 | export type NextRouter = 'pages' | 'app'; 2 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib" 3 | } 4 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "all", 3 | "printWidth": 100, 4 | "singleQuote": true, 5 | "semi": true, 6 | "tabWidth": 2 7 | } 8 | -------------------------------------------------------------------------------- /src/lib/types/ym.ts: -------------------------------------------------------------------------------- 1 | import { EventParameters } from './events'; 2 | 3 | export type YM = (tagID: number, ...parameters: EventParameters) => void; 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | node_modules 3 | 4 | # build 5 | dist 6 | 7 | # coverage 8 | coverage 9 | 10 | # misc 11 | .DS_Store 12 | .idea 13 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { YandexMetricaProvider } from './components/YandexMetricaProvider'; 2 | export { useMetrica } from './hooks/useMetrica'; 3 | export { ym } from './lib/ym'; 4 | -------------------------------------------------------------------------------- /jest.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from 'jest'; 2 | 3 | const config: Config = { 4 | preset: 'ts-jest', 5 | testEnvironment: 'jsdom', 6 | clearMocks: true, 7 | setupFilesAfterEnv: ['/jest-setup.ts'], 8 | transform: { 9 | '^.+\\.(js|jsx|ts|tsx)$': ['babel-jest', { presets: ['next/babel'] }], 10 | }, 11 | transformIgnorePatterns: ['/node_modules/'], 12 | }; 13 | 14 | export default config; 15 | -------------------------------------------------------------------------------- /src/lib/ym.ts: -------------------------------------------------------------------------------- 1 | import { EventParameters } from './types/events'; 2 | import { YM } from './types/ym'; 3 | 4 | export const ym = (tagID: number | null, ...parameters: EventParameters) => { 5 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment -- ym is defined by the Yandex.Metrica script 6 | // @ts-ignore 7 | const ym = window.ym as YM | undefined; 8 | 9 | if (!ym || !tagID) { 10 | return; 11 | } 12 | 13 | ym(tagID, ...parameters); 14 | }; 15 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2016", 4 | "module": "commonjs", 5 | "declaration": true, 6 | "esModuleInterop": true, 7 | "sourceMap": true, 8 | "outDir": "./dist", 9 | "forceConsistentCasingInFileNames": true, 10 | "strict": true, 11 | "skipLibCheck": true, 12 | "jsx": "react" 13 | }, 14 | "include": ["./src/**/*.ts", "./src/**/*.tsx"], 15 | "exclude": ["./src/**/*.test.ts", "./src/**/*.test.tsx"] 16 | } 17 | -------------------------------------------------------------------------------- /src/lib/types/options.ts: -------------------------------------------------------------------------------- 1 | import { VisitParameters } from './parameters'; 2 | 3 | export interface ExtLinkOptions { 4 | callback?: () => void; 5 | params?: VisitParameters; 6 | title?: string; 7 | } 8 | 9 | export interface FileOptions { 10 | callback?: () => void; 11 | params?: VisitParameters; 12 | referer?: string; 13 | title?: string; 14 | } 15 | 16 | export interface HitOptions { 17 | callback?: () => void; 18 | params?: VisitParameters; 19 | referer?: string; 20 | title?: string; 21 | } 22 | 23 | export interface NotBounceOptions { 24 | callback?: () => void; 25 | } 26 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: 4 | release: 5 | types: 6 | - created 7 | 8 | jobs: 9 | publish: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v3 14 | 15 | - uses: pnpm/action-setup@v2 16 | with: 17 | version: 8 18 | 19 | - uses: actions/setup-node@v3 20 | with: 21 | node-version: '20.x' 22 | registry-url: 'https://registry.npmjs.org' 23 | 24 | - name: Install 25 | run: pnpm install 26 | 27 | - name: Publish 28 | run: pnpm publish --no-git-checks 29 | env: 30 | NODE_AUTH_TOKEN: ${{ secrets.NPM_AUTH_TOKEN }} 31 | -------------------------------------------------------------------------------- /.github/workflows/check.yml: -------------------------------------------------------------------------------- 1 | name: Check 2 | 3 | on: 4 | push: 5 | 6 | jobs: 7 | check: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Checkout 11 | uses: actions/checkout@v3 12 | 13 | - uses: pnpm/action-setup@v2 14 | with: 15 | version: 8 16 | 17 | - name: Install 18 | run: pnpm install 19 | 20 | - name: Lint 21 | run: pnpm lint 22 | 23 | - name: Typecheck 24 | run: pnpm lint:ts 25 | 26 | - name: Format 27 | run: pnpm format:check 28 | 29 | - name: Build 30 | run: pnpm build 31 | 32 | - name: Test 33 | run: pnpm test:ci 34 | 35 | - name: Upload coverage 36 | uses: codecov/codecov-action@v4 37 | with: 38 | token: ${{ secrets.CODECOV_TOKEN }} 39 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true 5 | }, 6 | "settings": { 7 | "react": { 8 | "version": "detect" 9 | } 10 | }, 11 | "extends": [ 12 | "eslint:recommended", 13 | "plugin:react/recommended", 14 | "plugin:@typescript-eslint/recommended", 15 | "prettier" 16 | ], 17 | "parser": "@typescript-eslint/parser", 18 | "plugins": ["react", "react-hooks", "@typescript-eslint", "simple-import-sort"], 19 | "rules": { 20 | "react-hooks/rules-of-hooks": "error", 21 | "react-hooks/exhaustive-deps": "warn", 22 | "simple-import-sort/imports": "warn", 23 | "simple-import-sort/exports": "warn", 24 | "@typescript-eslint/no-unused-vars": ["warn", { "argsIgnorePattern": "^_" }] 25 | }, 26 | "ignorePatterns": ["node_modules", "dist"] 27 | } 28 | -------------------------------------------------------------------------------- /src/hooks/useTrackRouteChange.ts: -------------------------------------------------------------------------------- 1 | import { usePathname } from 'next/navigation'; 2 | import { Router } from 'next/router'; 3 | import { useEffect } from 'react'; 4 | 5 | import { NextRouter } from '../lib/types/router'; 6 | import { ym } from '../lib/ym'; 7 | 8 | export const useTrackRouteChange = ({ 9 | tagID, 10 | router, 11 | }: { 12 | tagID: number | null; 13 | router: NextRouter; 14 | }) => { 15 | const pathname = usePathname(); 16 | 17 | useEffect(() => { 18 | if (router === 'app') return; 19 | 20 | const handleRouteChange = (url: URL): void => { 21 | ym(tagID, 'hit', url.toString()); 22 | }; 23 | 24 | Router.events.on('routeChangeComplete', handleRouteChange); 25 | 26 | return () => { 27 | Router.events.off('routeChangeComplete', handleRouteChange); 28 | }; 29 | }, [tagID, router]); 30 | 31 | useEffect(() => { 32 | if (router === 'pages') return; 33 | 34 | ym(tagID, 'hit', pathname); 35 | }, [tagID, router, pathname]); 36 | }; 37 | -------------------------------------------------------------------------------- /src/lib/__tests__/ym.test.ts: -------------------------------------------------------------------------------- 1 | import { ym } from '../ym'; 2 | 3 | const YM_MOCK = jest.fn(); 4 | Object.defineProperty(window, 'ym', { 5 | value: YM_MOCK, 6 | writable: true, 7 | }); 8 | 9 | describe('ym', () => { 10 | it('calls ym with provided parameters', () => { 11 | ym(444, 'hit', '/url'); 12 | ym(444, 'reachGoal', 'goal'); 13 | 14 | expect(YM_MOCK).toHaveBeenCalledTimes(2); 15 | expect(YM_MOCK).toHaveBeenNthCalledWith(1, 444, 'hit', '/url'); 16 | expect(YM_MOCK).toHaveBeenNthCalledWith(2, 444, 'reachGoal', 'goal'); 17 | }); 18 | 19 | it('does not call ym if tagID is not provided', () => { 20 | ym(null, 'hit', '/url'); 21 | ym(null, 'reachGoal', 'goal'); 22 | 23 | expect(YM_MOCK).not.toHaveBeenCalled(); 24 | }); 25 | 26 | it('does not call ym if ym is not defined', () => { 27 | Object.defineProperty(window, 'ym', { 28 | value: undefined, 29 | writable: true, 30 | }); 31 | 32 | ym(444, 'hit', '/url'); 33 | ym(444, 'reachGoal', 'goal'); 34 | 35 | expect(YM_MOCK).not.toHaveBeenCalled(); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Vladislav Doronin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/lib/types/parameters.ts: -------------------------------------------------------------------------------- 1 | export interface VisitParameters { 2 | order_price?: number; 3 | currency?: string; 4 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 5 | [key: string]: any; 6 | } 7 | 8 | export interface UserParameters { 9 | UserID?: number; 10 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 11 | [key: string]: any; 12 | } 13 | 14 | export interface InitParameters { 15 | accurateTrackBounce?: boolean | number; 16 | childIframe?: boolean; 17 | clickmap?: boolean; 18 | defer?: boolean; 19 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 20 | ecommerce?: boolean | string | any[]; 21 | params?: VisitParameters | VisitParameters[]; 22 | userParams?: UserParameters; 23 | trackHash?: boolean; 24 | trackLinks?: boolean; 25 | trustedDomains?: string[]; 26 | type?: number; 27 | webvisor?: boolean; 28 | triggerEvent?: boolean; 29 | } 30 | 31 | export interface FirstPartyParamsParameters { 32 | email?: string; 33 | phone_number?: string; 34 | first_name?: string; 35 | last_name?: string; 36 | home_address?: string; 37 | street?: string; 38 | city?: string; 39 | region?: string; 40 | postal_code?: string; 41 | country?: string; 42 | yandex_cid?: number; 43 | } 44 | -------------------------------------------------------------------------------- /src/hooks/useMetrica.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useContext } from 'react'; 2 | 3 | import { MetricaTagIDContext } from '../components/YandexMetricaProvider'; 4 | import { type EventParameters } from '../lib/types/events'; 5 | import { type NotBounceOptions } from '../lib/types/options'; 6 | import { type UserParameters, type VisitParameters } from '../lib/types/parameters'; 7 | import { ym } from '../lib/ym'; 8 | 9 | export const useMetrica = () => { 10 | const tagID = useContext(MetricaTagIDContext); 11 | 12 | const notBounce = useCallback( 13 | (options?: NotBounceOptions) => { 14 | ym(tagID, 'notBounce', options); 15 | }, 16 | [tagID], 17 | ); 18 | 19 | const reachGoal = useCallback( 20 | (target: string, params?: VisitParameters, callback?: () => void) => { 21 | ym(tagID, 'reachGoal', target, params, callback); 22 | }, 23 | [tagID], 24 | ); 25 | 26 | const setUserID = useCallback( 27 | (userID: string) => { 28 | ym(tagID, 'setUserID', userID); 29 | }, 30 | [tagID], 31 | ); 32 | 33 | const userParams = useCallback( 34 | (parameters: UserParameters) => { 35 | ym(tagID, 'userParams', parameters); 36 | }, 37 | [tagID], 38 | ); 39 | 40 | const ymEvent = useCallback( 41 | (...parameters: EventParameters) => { 42 | ym(tagID, ...parameters); 43 | }, 44 | [tagID], 45 | ); 46 | 47 | return { notBounce, reachGoal, setUserID, userParams, ymEvent }; 48 | }; 49 | -------------------------------------------------------------------------------- /src/hooks/__tests__/useTrackRouteChange.test.ts: -------------------------------------------------------------------------------- 1 | import { act, renderHook } from '@testing-library/react'; 2 | import { usePathname } from 'next/navigation'; 3 | import { Router } from 'next/router'; 4 | 5 | import { useTrackRouteChange } from '../useTrackRouteChange'; 6 | 7 | const YM_MOCK = jest.fn(); 8 | Object.defineProperty(window, 'ym', { 9 | value: YM_MOCK, 10 | writable: true, 11 | }); 12 | 13 | jest.mock('next/navigation', () => ({ 14 | usePathname: jest.fn(), 15 | })); 16 | 17 | describe('useTrackRouteChange', () => { 18 | it('handles route change for pages router', () => { 19 | renderHook(() => useTrackRouteChange({ tagID: 444, router: 'pages' })); 20 | 21 | Router.events.emit('routeChangeStart'); 22 | 23 | expect(YM_MOCK).not.toHaveBeenCalled(); 24 | 25 | Router.events.emit('routeChangeComplete', 'https://test.com/'); 26 | 27 | expect(YM_MOCK).toHaveBeenCalledTimes(1); 28 | expect(YM_MOCK).toHaveBeenCalledWith(444, 'hit', 'https://test.com/'); 29 | }); 30 | 31 | it('handles route change for app router', () => { 32 | (usePathname as jest.Mock).mockReturnValue('/initial'); 33 | const onSpy = jest.spyOn(Router.events, 'on'); 34 | 35 | const { rerender } = renderHook(() => useTrackRouteChange({ tagID: 444, router: 'app' })); 36 | 37 | expect(YM_MOCK).toHaveBeenCalledTimes(1); 38 | expect(YM_MOCK).toHaveBeenLastCalledWith(444, 'hit', '/initial'); 39 | 40 | expect(onSpy).not.toHaveBeenCalled(); 41 | 42 | (usePathname as jest.Mock).mockReturnValue('/second'); 43 | 44 | act(() => { 45 | rerender(); 46 | }); 47 | 48 | expect(YM_MOCK).toHaveBeenCalledTimes(2); 49 | expect(YM_MOCK).toHaveBeenLastCalledWith(444, 'hit', '/second'); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /src/hooks/__tests__/useMetrica.test.tsx: -------------------------------------------------------------------------------- 1 | import { renderHook } from '@testing-library/react'; 2 | import React, { FC, ReactNode } from 'react'; 3 | 4 | import { MetricaTagIDContext } from '../../components/YandexMetricaProvider'; 5 | import { useMetrica } from '../useMetrica'; 6 | 7 | const YM_MOCK = jest.fn(); 8 | Object.defineProperty(window, 'ym', { 9 | value: YM_MOCK, 10 | writable: true, 11 | }); 12 | 13 | const Providers: FC<{ children: ReactNode }> = ({ children }) => { 14 | return {children}; 15 | }; 16 | 17 | describe('useMetrica', () => { 18 | it('calls ym methods with correct parameters', () => { 19 | const { result } = renderHook(() => useMetrica(), { wrapper: Providers }); 20 | const { notBounce, reachGoal, setUserID, userParams, ymEvent } = result.current; 21 | 22 | notBounce(); 23 | 24 | expect(YM_MOCK).toHaveBeenCalledWith(444, 'notBounce', undefined); 25 | 26 | reachGoal('test', { order_price: 999 }); 27 | 28 | expect(YM_MOCK).toHaveBeenCalledWith(444, 'reachGoal', 'test', { order_price: 999 }, undefined); 29 | 30 | setUserID('12345'); 31 | 32 | expect(YM_MOCK).toHaveBeenCalledWith(444, 'setUserID', '12345'); 33 | 34 | userParams({ status: 'Gold', UserID: 12345 }); 35 | 36 | expect(YM_MOCK).toHaveBeenCalledWith(444, 'userParams', { status: 'Gold', UserID: 12345 }); 37 | 38 | ymEvent('extLink', 'https://example.com/', { title: 'Test', params: { order_price: 999 } }); 39 | 40 | expect(YM_MOCK).toHaveBeenCalledWith(444, 'extLink', 'https://example.com/', { 41 | title: 'Test', 42 | params: { order_price: 999 }, 43 | }); 44 | 45 | expect(YM_MOCK).toHaveBeenCalledTimes(5); 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "next-yandex-metrica", 3 | "version": "1.2.3", 4 | "description": "Yandex Metrica integration for Next.js", 5 | "files": [ 6 | "dist" 7 | ], 8 | "main": "dist/index.js", 9 | "types": "dist/index.d.ts", 10 | "repository": "https://github.com/v-doronin/next-yandex-metrica", 11 | "author": "Vladislav Doronin ", 12 | "license": "MIT", 13 | "keywords": [ 14 | "next", 15 | "next.js", 16 | "yandex", 17 | "metrica", 18 | "metrika", 19 | "analytics" 20 | ], 21 | "scripts": { 22 | "build": "del-cli dist && tsc", 23 | "prepublishOnly": "pnpm build", 24 | "lint": "eslint src --fix --max-warnings 0", 25 | "lint:ts": "tsc --noEmit", 26 | "test": "jest", 27 | "test:ci": "jest --ci --coverage", 28 | "format": "prettier --write --log-level silent .", 29 | "format:check": "prettier --check ." 30 | }, 31 | "devDependencies": { 32 | "@testing-library/jest-dom": "^6.1.5", 33 | "@testing-library/react": "^14.1.2", 34 | "@types/jest": "^29.5.11", 35 | "@types/node": "^20.10.4", 36 | "@types/react": "^18.2.43", 37 | "@typescript-eslint/eslint-plugin": "^6.13.2", 38 | "@typescript-eslint/parser": "^6.13.2", 39 | "del-cli": "^5.1.0", 40 | "eslint": "^8.55.0", 41 | "eslint-config-prettier": "^9.1.0", 42 | "eslint-plugin-react": "^7.33.2", 43 | "eslint-plugin-react-hooks": "^4.6.0", 44 | "eslint-plugin-simple-import-sort": "^10.0.0", 45 | "jest": "^29.7.0", 46 | "jest-environment-jsdom": "^29.7.0", 47 | "next": "^14.0.4", 48 | "prettier": "^3.1.1", 49 | "react": "^18.2.0", 50 | "react-dom": "^18.2.0", 51 | "ts-jest": "^29.1.1", 52 | "ts-node": "^10.9.2", 53 | "typescript": "^5.3.3" 54 | }, 55 | "peerDependencies": { 56 | "next": ">=11.0.0", 57 | "react": ">=17.0.0" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/lib/types/events.ts: -------------------------------------------------------------------------------- 1 | import { ExtLinkOptions, FileOptions, HitOptions, NotBounceOptions } from './options'; 2 | import { 3 | FirstPartyParamsParameters, 4 | InitParameters, 5 | UserParameters, 6 | VisitParameters, 7 | } from './parameters'; 8 | 9 | type InitEventParameters = [eventName: 'init', parameters: InitParameters]; 10 | 11 | type AddFileExtensionEventParameters = [ 12 | eventName: 'addFileExtension', 13 | extensions: string | string[], 14 | ]; 15 | 16 | type ExtLinkEventParameters = [eventName: 'extLink', url: string, options?: ExtLinkOptions]; 17 | 18 | type FileEventParameters = [eventName: 'file', url: string, options?: FileOptions]; 19 | 20 | type FirstPartyParamsEventParameters = [ 21 | eventName: 'firstPartyParams', 22 | parameters: FirstPartyParamsParameters, 23 | ]; 24 | 25 | type GetClientIDEventParameters = [eventName: 'getClientID', cb: (clientID: string) => void]; 26 | 27 | type HitEventParameters = [eventName: 'hit', url: string, options?: HitOptions]; 28 | 29 | type NotBounceEventParameters = [eventName: 'notBounce', options?: NotBounceOptions]; 30 | 31 | type ParamsEventParameters = [eventName: 'params', parameters: VisitParameters | VisitParameters[]]; 32 | 33 | type ReachGoalEventParameters = [ 34 | eventName: 'reachGoal', 35 | target: string, 36 | params?: VisitParameters, 37 | callback?: () => void, 38 | ]; 39 | 40 | type SetUserIDEventParameters = [eventName: 'setUserID', userID: string]; 41 | 42 | type UserParamsEventParameters = [eventName: 'userParams', parameters: UserParameters]; 43 | 44 | export type EventParameters = 45 | | InitEventParameters 46 | | AddFileExtensionEventParameters 47 | | ExtLinkEventParameters 48 | | FileEventParameters 49 | | FirstPartyParamsEventParameters 50 | | GetClientIDEventParameters 51 | | HitEventParameters 52 | | NotBounceEventParameters 53 | | ParamsEventParameters 54 | | ReachGoalEventParameters 55 | | SetUserIDEventParameters 56 | | UserParamsEventParameters; 57 | -------------------------------------------------------------------------------- /src/components/YandexMetricaProvider.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import Script, { ScriptProps } from 'next/script'; 4 | import React, { createContext, FC, ReactNode, useMemo } from 'react'; 5 | 6 | import { useTrackRouteChange } from '../hooks/useTrackRouteChange'; 7 | import { InitParameters } from '../lib/types/parameters'; 8 | import { NextRouter } from '../lib/types/router'; 9 | 10 | export const MetricaTagIDContext = createContext(null); 11 | 12 | interface Props { 13 | children: ReactNode; 14 | tagID?: number; 15 | strategy?: ScriptProps['strategy']; 16 | initParameters?: InitParameters; 17 | shouldUseAlternativeCDN?: boolean; 18 | router: NextRouter; 19 | } 20 | 21 | export const YandexMetricaProvider: FC = ({ 22 | children, 23 | tagID, 24 | strategy = 'afterInteractive', 25 | initParameters, 26 | shouldUseAlternativeCDN = false, 27 | router, 28 | }) => { 29 | const YANDEX_METRICA_ID = process.env.NEXT_PUBLIC_YANDEX_METRICA_ID; 30 | const id = useMemo(() => { 31 | return tagID || (YANDEX_METRICA_ID ? Number(YANDEX_METRICA_ID) : null); 32 | }, [YANDEX_METRICA_ID, tagID]); 33 | 34 | useTrackRouteChange({ tagID: id, router }); 35 | 36 | if (!id) { 37 | console.warn('[next-yandex-metrica] Yandex.Metrica tag ID is not defined'); 38 | 39 | return <>{children}; 40 | } 41 | 42 | const scriptSrc = shouldUseAlternativeCDN 43 | ? 'https://cdn.jsdelivr.net/npm/yandex-metrica-watch/tag.js' 44 | : 'https://mc.yandex.ru/metrika/tag.js'; 45 | 46 | return ( 47 | <> 48 | 58 | {/** Using dangerouslySetInnerHTML to bypass Next.js image optimization which interferes with Yandex tracking pixel 59 | * @see https://github.com/vercel/next.js/issues/56882 60 | */} 61 |