Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

NextJs 13 & app router #99

Open
revolunet opened this issue Apr 17, 2023 · 12 comments
Open

NextJs 13 & app router #99

revolunet opened this issue Apr 17, 2023 · 12 comments
Labels
bug Something isn't working help wanted Extra attention is needed

Comments

@revolunet
Copy link
Member

revolunet commented Apr 17, 2023

Looks like the new "app router" in NextJS@13 removed the router.events.

A fix is required to handle this news navigation pattern

vercel/next.js#42016

some example implementation : SocialGouv/mda#286

@revolunet revolunet added bug Something isn't working help wanted Extra attention is needed labels Apr 17, 2023
@revolunet revolunet changed the title NextJs 13 NextJs 13 & app router Apr 17, 2023
@vincentwinkel
Copy link

any update?

@colonder
Copy link

colonder commented Aug 8, 2023

What about this approach, maybe this could be used? https://sdorra.dev/posts/2022-11-11-next-with-fathom

@eivindml
Copy link

eivindml commented Oct 20, 2023

Also looking into this. Anyone got it working?

@colonder
Copy link

I created such component but I used it just as a standalone one placed in the code like any other. Any help to make it align with current implementation and make it backwards-compatible will be appreciated.

"use client";

import { usePathname, useSearchParams } from "next/navigation";
import React, { useEffect, useState } from "react";
import { Suspense } from 'react'
import { getCookieConsentValue } from "react-cookie-consent";

declare global {
  interface Window {
    _paq: any;
  }
}

const isExcludedUrl = (url: string, patterns: string[]): boolean => {
  let excluded = false;
  patterns.forEach((pattern) => {
    if (new RegExp(pattern).exec(url) !== null) {
      excluded = true;
    }
  });
  return excluded;
};

interface InitSettings {
  url?: string;
  siteId?: string;
  jsTrackerFile?: string;
  phpTrackerFile?: string;
  excludeUrlsPatterns?: string[];
  onRouteChangeStart?: (path: string) => void;
  onRouteChangeComplete?: (path: string) => void;
  onInitialization?: () => void;
}

interface Dimensions {
  dimension1?: string;
  dimension2?: string;
  dimension3?: string;
  dimension4?: string;
  dimension5?: string;
  dimension6?: string;
  dimension7?: string;
  dimension8?: string;
  dimension9?: string;
  dimension10?: string;
}

// to push custom events
export function push(
  args: (
    | Dimensions
    | number[]
    | string[]
    | number
    | string
    | null
    | undefined
  )[]
): void {
  if (!window._paq) {
    window._paq = [];
  }
  window._paq.push(args);
}

const startsWith = (str: string, needle: string) => {
  return str.substring(0, needle.length) === needle;
};

function Tracker({
  url,
  siteId,
  jsTrackerFile = "matomo.js",
  phpTrackerFile = "matomo.php",
  excludeUrlsPatterns = [],
  onRouteChangeStart = undefined,
  onRouteChangeComplete = undefined,
  onInitialization = undefined,
}: InitSettings) {
  const pathname = usePathname();
  const searchParams = useSearchParams();
  const [prevPath, setPrevPath] = useState(pathname);

  useEffect(() => {
    window._paq = window._paq !== null ? window._paq : [];
    if (!url) {
      console.warn("Matomo disabled, please provide matomo url");
      return;
    }
    // order is important -_- so campaign are detected
    const excludedUrl = typeof window !== "undefined" && isExcludedUrl(window.location.pathname, excludeUrlsPatterns);

    if (onInitialization) onInitialization();

    if (getCookieConsentValue("haczykowskaConsent")) {
      push(["forgetUserOptOut"]);
      push(["rememberCookieConsentGiven"]);
    } else {
      push(["optUserOut"]);
      push(["forgetCookieConsentGiven"]);
      push(["requireCookieConsent"]);
    }
    push(["enableHeartBeatTimer"]);
    push(["disableQueueRequest"]);
    push(["enableLinkTracking"]);
    push(["setTrackerUrl", `${url}/${phpTrackerFile}`]);
    push(["setSiteId", siteId]);

    if (excludedUrl) {
      if (typeof window !== "undefined") {
        console.log(`matomo: exclude track ${window.location.pathname}`);
      }
    } else {
      push(["trackPageView"]);
    }

    /**
     * for initial loading we use the location.pathname
     * as the first url visited.
     * Once user navigate across the site,
     * we rely on Router.pathname
     */
    const scriptElement = document.createElement("script");
    const refElement = document.getElementsByTagName("script")[0];
    scriptElement.type = "text/javascript";
    scriptElement.async = true;
    scriptElement.defer = true;
    scriptElement.src = `${url}/${jsTrackerFile}`;
    if (refElement.parentNode) {
      refElement.parentNode.insertBefore(scriptElement, refElement);
    }
  }, [])

  useEffect(() => {
    if (!pathname || getCookieConsentValue("haczykowskaConsent")) {
      return;
    }

    if (!prevPath) {
      return setPrevPath(pathname);
    }

    push(["setReferrerUrl", `${prevPath}`]);
    push(["setCustomUrl", pathname]);
    push(["deleteCustomVariables", "page"]);
    setPrevPath(pathname);
    if (onRouteChangeStart) onRouteChangeStart(pathname);
    // In order to ensure that the page title had been updated,
    // we delayed pushing the tracking to the next tick.
    setTimeout(() => {
      push(["setDocumentTitle", document.title]);
      if (!!searchParams) {
        push(["trackSiteSearch", searchParams.get("keyword") ?? ""]);
      } else {
        push(["trackPageView"]);
      }
    }, 0);

    if (onRouteChangeComplete) onRouteChangeComplete(pathname);

  }, [pathname, searchParams, prevPath, excludeUrlsPatterns, onRouteChangeComplete, onRouteChangeStart]);

  return null;
}

const MatomoTracker = (props: InitSettings) => {
  return (
    <Suspense fallback={null}>
      <Tracker {...props} />
    </Suspense>
  )
}

export default MatomoTracker

@eivindml
Copy link

Thanks for the snippet. It does work, but it looks like it tracks page views very sporadically?

@colonder
Copy link

colonder commented Oct 23, 2023

Maybe, I noticed that too. Eventually, I abandoned Matomo and implemented Google Tag Manager, so feel free to modify my snippet however you like, maybe you will make it work as it should be.

@laem
Copy link

laem commented Mar 18, 2024

This is an attempt to set up Matomo for a simpler case, with no GDPR consent banner.

Might need some adjustments, I'll watch the Matomo events.

https://github.com/betagouv/reno/blob/master/utils/Matomo.tsx

@JacquesVergine
Copy link

Thanks laem, very useful, I used your code attempt, and it is tracking. However, and I might be wrong, it doesn't seem to track the different paths, I can only see the root path in Matomo.

I've pushed a custom URL to Matomo and now it's working. On line 26:
push(['setCustomUrl', pathName + searchParamsString]);

@laem
Copy link

laem commented Mar 27, 2024

Thanks ! I can see different paths on my dashboard, but it may only be the initial page paths, not the subsequent ones.
Thanks !

Capture d’écran 2024-03-27 à 17 23 07

@jerommiole
Copy link

Thanks @laem ! your repo is is very useful 😁

Check the repo here https://github.com/betagouv/reno/blob/master/utils/Matomo.tsx,
and on line 26 add: push(['setCustomUrl', pathName + searchParamsString]);

@laem
Copy link

laem commented Apr 2, 2024

Thanks, done !

@ziaq
Copy link

ziaq commented Jun 3, 2024

Implementation with prevention of double tracking on the first site visit.

'use client'

import { init, push } from '@socialgouv/matomo-next';
import { usePathname } from 'next/navigation';
import { useEffect, useRef  } from 'react';

const MATOMO_URL = 'https://your-domain.matomo.cloud/';
const MATOMO_SITE_ID = '1';

export function MatomoAnalytics() {
  const pathname = usePathname();
  const isInitialLoad = useRef(true);

  useEffect(() => {
    init({ url: MATOMO_URL, siteId: MATOMO_SITE_ID });
    return () => push(['HeatmapSessionRecording::disable']);
  }, []);

  useEffect(() => {
    if (isInitialLoad.current) {
      isInitialLoad.current = false;

    } else {
      if (pathname) {
        push(['setCustomUrl', pathname]);
        push(['trackPageView']);
      }
    }
  }, [pathname])

  return null
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working help wanted Extra attention is needed
Projects
None yet
Development

No branches or pull requests

8 participants