So basically the problem i have is that my site is using reactjs so it doesn't reload and i need to detect the change on the URL in order to send a new pageview to Google Analytics. Has anyone deal with this before?
EDIT: Maybe i was a little unclear, im using Google Tag Manager and i have no control over the code on the page but i can request for dataLayers.
The obvious solution is to ask for a dataLayer when the page changes but since i would like to avoid doing this i was strictly asking if anyone knows a way to detect this kind of changes on the DOM from GTM.
For single page apps, you can track 'virtual' pageviews like given in the docs:
When the page changes, do
ga('set', 'page', '/new-page.html');
After this point, if your do a 'send', it will track this page.
ga('send', 'pageview');
I'd suggest you use something like react-ga for doing it a little more conveniently, it has functions like
ReactGA.pageview('/about/contact-us');
and ReactGA.modalview('/about/contact-us');
You need to look for all the paths using a Route like this,
<Route path="/" component={updateTracking} />
<Switch>
..... //further actual routes
</Switch>
and then send the pageview using the global Google Analytics (ga) method (accessible through the window object) using window.location.pathname
const updateTracking = () => {
window.ga('send', 'pageview', {
page: window.location.pathname
});
}
Note: You need to put the tracking code you got from Google Analytics in the main HTML (index.html) for it all to work.
Alternatively, you can listen for createBrowserHistory changes and send a pageview event.
import { createBrowserHistory } from 'history'
const history = createBrowserHistory()
history.listen(() => {
window.ga('send', 'pageview', {
page: window.location.pathname
})
}
)
The useLocation hook was added in 5.1.0 & can be used:
In your App.js, import the following
import React, { useState } from 'react';
import { useLocation } from "react-router-dom";
The following assumes you have created the react application using create-react-app or some other mechanism to pick GA_ID in your application from ENV
In your Environment variables, make sure you have : REACT_APP_GA_ID
function initialiseAnalytics() {
const TRACKING_ID = process.env.REACT_APP_GA_ID;
ReactGA.initialize(TRACKING_ID);
}
function usePageTracking() {
const location = useLocation();
const [initialized, setInitialized] = useState(false);
useEffect(() => {
initialiseAnalytics();
setInitialized(true);
}, []);
useEffect(() => {
if (initialized) {
ReactGA.pageview(location.pathname + location.search);
}
}, [initialized, location]);
}
Finally, in your App function, call the page tracking -
function App() {
usePageTracking();
// Other Code Goes Here
return (
<div className="App">
<p>Sample Application</p>
</div>
);
}
Anyone still looking for the answer, here it is.
This is the expected behavior for Single Page Application(SPA) like React.
Solution is to update the Tracker when your route changes
You can deal with this problem in two ways
1- In all your route containers use set command from ga and update the tracker
ga('set', 'page', '/new-page.html');
2- In all your route containers use send command from ga and update the tracker
ga('send', 'pageview', '/new-page.html');
The second solution using send command is not recommended by Google Analytics
This is because fields passed via the send command are not set on the tracker—they apply to the current hit only. Not updating the tracker will cause problems if your application sends any non-pageview hits (e.g. events or social interactions), as those hits will be associated with whatever page value the tracker had when it was created.
Ref: Google Anaylytics
Related
Context & Reproducible Scenario
I'm using the combination of these libraries and tools:
NextJS 12+ (based on React 18+)
MSAL-Browser 2.25+ and MSAL-React 1.6+ (Microsoft's libs for OpenID login against Azure B2C)
I'm using the Auth Code + PKCE redirect flow so this is the flow for users:
They land on /, the home page
They click a /me router link
They go to Azure B2C to log in because said page has this logic:
<MsalAuthenticationTemplate
interactionType={InteractionType.Redirect}
authenticationRequest={loginRequest}>
where loginRequest.state is set to router.asPath (the "intended" page: /me)
Note that the page is also wrapped in a <NoSsr> component based off Stack Overflow.
User logs in on Azure B2C, gets redirected back to my app at / (the root)
⛔ Problem: the user now briefly sees the / (home) page
After a very brief moment, the user gets sent to /me where they are signed in
The MSAL docs don't seem to have much on the state property from OIDC or this redirect behavior, and I can't find much about this in the MSAL sample for NextJS either.
In short: the issue
How do I make sure MSAL-React in my NextJS application send users to the "intended" page immediately on startup, without briefly showing the root page where the Identity Server redirects to?
Relevant extra information
Here's my custom _app.js component, which seems relevant because it is a component that triggers handleRedirectPromise which causes the redirect to intended page:
export default function MyApp({ Component, pageProps }) {
return (
<MsalProvider instance={msalInstance}>
<PageHeader></PageHeader>
<Component {...pageProps} />
</MsalProvider>
);
}
PS. To help folks searching online find this question: the behavior is triggered by navigateToLoginRequestUrl: true (is the default) in the configuration. Setting it to false plainly disables sending the user to the intended page at all.
Attempted solutions with middleware
I figured based on how APP_INITIALIZERs work in Angular, to use middleware like this at some point:
// From another file:
// export const msalInstance = new PublicClientApplication(msalConfig);
export async function middleware(_request) {
const targetUrlAfterLoginRedirect = await msalInstance.handleRedirectPromise()
.then((result) => {
if (!!result && !!result.state) {
return result.state;
}
return null;
});
console.log('Found intended target before login flow: ', targetUrlAfterLoginRedirect);
// TODO: Send user to the intended page with router.
}
However, this logs on the server's console:
Found intended target before login flow: null
So it seems middleware is too early for msal-react to cope with? Shame, because middleware would've been perfect, to allow as much SSR for target pages as possible.
It's not an option to change the redirect URL on B2C's side, because I'll be constantly adding new routes to my app that need this behavior.
Note that I also tried to use middleware to just sniff out the state myself, but since the middleware runs on Node it won't have access to the hash fragment.
Animated GIF showing the flashing home page
Here's an animated gif that shows the /home page is briefly (200ms or so) shown before /me is properly opened. Warning, gif is a wee bit flashy so in a spoiler tag:
Attempted solution with custom NavigationClient
I've tried adding a custom NavigationClient to more closely mimic the nextjs sample from Microsoft's repository, like this:
import { NavigationClient } from "#azure/msal-browser";
// See: https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-react/docs/performance.md#how-to-configure-azuremsal-react-to-use-your-routers-navigate-function-for-client-side-navigation
export class CustomNavigationClient extends NavigationClient {
constructor(router) {
super();
this.router = router;
}
async navigateInternal(url, options) {
console.log('👍 Navigating Internal to', url);
const relativePath = url.replace(window.location.origin, "");
if (options.noHistory) {
this.router.replace(relativePath);
} else {
this.router.push(relativePath);
}
return false;
}
}
This did not solve the issue. The console.log is there allowing me to confirm this code is not run on the server, as the Node logs don't show it.
Attempted solution: go through MSAL's SSR docs
Another thing I've tried is going through the documentation claiming #azure/msal-react supports Server Side Rendering (SSR) but those docs nor the linked samples demonstrate how to solve my issue.
Attempted solution in _app.tsx
Another workaround I considered was to sniff out the hash fragment client side when the user returns to my app (and make sure the intended page is also in that state). I can successfully send the OpenID state to B2C like this...
const extendedAuthenticationRequest = {
...authenticationRequest,
state: `~path~${asPath}~path~`,
};
...and see it returned in the Network tab of the dev tools.
However, when I try to extract it in my _app.tsx still doesn't work. I tried this code from another Stack Overflow answer to get the .hash:
const [isMounted, setMounted] = useState(false);
useEffect(() => {
if (isMounted) {
console.log('====> saw the following hash', window.location.hash);
const matches = /~path~(.+)~path~/.exec(window.location.hash);
if (matches && matches.length > 0 && matches[1]) {
const targetUrlAfterOpenIdRedirect = decodeURIComponent(matches[1]);
console.log("Routing to", targetUrlAfterOpenIdRedirect);
router.replace(targetUrlAfterOpenIdRedirect);
}
} else {
setMounted(true);
}
}, [isMounted]);
if (!isMounted) return null;
// else: render <MsalProvider> and the intended page component
This does find the intended page from the state and executes routing, but still flashes the /home page before going to the intended page.
Footnote: related GitHub issue
Submitted an issue at MSAL's GitHub repository too.
I have used js-cookie in my React application, here I just use a simple condition to check whether the cookie is matched or not and push history to the following route, here is my code:
const readCookie = () => {
let cookie = false;
if(cookie = Cookies.get("isLogged")){
history.push('/Homepage');
setAuthenticated(true);
}else{
history.push('/');
}
}
useEffect(() => {
readCookie()
},[]);
But the problem is, if I have many routes, so I just have to write many if conditions as much as my existing routes to check and push to that specific route, I think it doesn't make sense to do that. So, I just want to know that is there any easier way? To check the cookies and push to their paths or routes.
Yes, you need to create Public and Private Routes please follow the steps mentioned in the link below:
https://medium.com/wesionary-team/private-and-public-routes-with-react-router-e5f814ccff2f
and you can also check this repo
https://github.com/thanhbinhtran93/react-router-example
I have one quick and dirt question.
Is it possible to catch a query parameter from a server redirect inside of Gatsby.js application?
We have a Pardot tracking link that does redirect to our thank you page which is built in Gatsby.js and I want to pass some query parameters to the application it self from that redirect.
So for example:
www.trackedlink.com/thank-you?programme_code=CODE_FROM_REDIRECT_ON_SERVERSIDE
will redirect to:
www.gatsbyapplicationthatwillreadthequery.com/thank-you?programme_code=CODE_FROM_REDIRECT_ON_SERVERSIDE
Is it possible to read that query inside of the application if it's coming from the outside of the app?
Cheers and have a great week!
If they are triggered in the client-side the redirection will be caught by the application and yes, it would possible if they are coming from outside the app or using a standard anchor. Not using a #reach/router (<Link> component since it's a limitation).
A clean and scalable way to use it is by adding in the function in your gatsby-browser.js configuration:
import React from 'react';
import { checkUrlFunction } from './src/services/yourCheckUrlFunction';
export const onClientEntry = () => checkUrlFunction();
Adding a function in gatsby-browser.js with onClientEntry API will trigger your function once the page is loaded. From the documentation:
onClientEntry Function (_: emptyArg, pluginOptions: pluginOptions) => undefined Called when the Gatsby browser runtime first starts.
Your function should look like:
export const checkUrlFunction = () => {
if (typeof window !== 'undefined') {
const queryString = window.location.search;
const urlParams = new URLSearchParams(queryString);
const programmeCode= urlParams.get('programme_code')
if(programmeCode) window.localStorage.setItem('programmeCode', programmeCode)
console.log(programmeCode); // will output CODE_FROM_REDIRECT_ON_SERVERSIDE
};
};
Note the typeof window !== 'undefined' necessary to avoid issues if the window object is not defined when triggering the function
Hi Ferran, thank you for your solution but unfortunately, it does not
work when the redirect happens. It only works if the query string is
inside of the application
Yes, the idea of adding the function in gatsby-browser.js is to avoid the addition of checkUrlFunction() in each page, template, or component. The disadvantage is that you lose a bit of control but it saves a lot of overwriting code and improves the scalability and readability.
Thanks, Ferran, if you could show me an example of it - it would be
amazing! This cookie topic is sort of unknown water for me
So, with your specifications updated, I've added the localStorage approach since it's easier to achieve in a non-IDE environment like this, but the idea is exactly the same.
Set a vault (cookie or localStorage) automated in the gatsby-browser.js function
if(programmeCode) window.localStorage.setItem('programmeCode', programmeCode)
This sets a localStorage key/value pair ('programmeCode' (key)/programmeCode (value)
Access to that vault in your component. Use a componentDidMount lifecycle or useEffect hook to ensure that is loaded before the DOM tree is mounted.
useEffect(()=>{
if(typeof window !== undefined) console.log(window.location.getItem('programmeCode')
}, [])
I have setup gatsby project using this link. It is working correctly.
Now I know how to create route by defining the component inside the pages folder. But now I have a new challenge I need to create one dynamic route so that I can pass my id in it (Just like reactjs).
<Route path: "/path/:id"/>
How do I do that in gatsby?
You have to explicitly tell gatsby that a path should be dynamic. From the docs:
// gatsby-node.js
// Implement the Gatsby API “onCreatePage”. This is
// called after every page is created.
exports.onCreatePage = async ({ page, actions }) => {
const { createPage } = actions
// page.matchPath is a special key that's used for matching pages
// only on the client.
if (page.path.match(/^\/app/)) {
page.matchPath = "/app/*"
// Update the page.
createPage(page)
}
}
and then you can use dynamic routing in src/pages/app.js
import { Router } from "#reach/router"
const SomeSubPage = props => {
return <div>Hi from SubPage with id: {props.id}</div>
}
const App = () => (
<Layout>
<Link to="/app/1">First item</Link>{" "}
<Link to="/app/2">Second item</Link>{" "}
<Router>
// ...dynamic routes here
<SomeSubPage path="/app/:id" />
</Router>
</Layout>
)
export default App
Everything that goes to /app/* will be handled dynamically now. You should find your id as usual in the props.
Have a look at their authentication example https://github.com/gatsbyjs/gatsby/tree/master/examples/simple-auth
You can use square brackets ([ ]) in the file path to mark any dynamic segments of the URL. For example, in order to edit a user, you might want a route like /user/:id to fetch the data for whatever id is passed into the URL.
src/pages/users/[id].js will generate a route like /users/:id
src/pages/users/[id]/group/[groupId].js will generate a route like /users/:id/group/:groupId
Reference: https://www.gatsbyjs.com/docs/reference/routing/file-system-route-api#creating-client-only-routes
You can use gatsby-plugin-create-client-paths. It uses matchPath. For more info check
https://www.gatsbyjs.org/docs/gatsby-internals-terminology/#matchpath
https://www.gatsbyjs.org/packages/gatsby-plugin-create-client-paths/
This answer is Super late, but for anyone in the future who is faced with this problem, I have a simpler solution.
In Gatsby terms it's called a Splat Route.
For examples, If you want some page "domain.com/profile/[id]", where id can be any number, which will be used to display different data inside the website, you should name your page as [...id].
Now inside the page you can access this id as
const ProfilePage = (props) => <div>This page is for id number {props.params.id}</div>
Note: Don't miss the 3 dots, that is what signifies a splat route in gatsby.
I have a settings page, with a few sub routes:
--/home
--/articles
--/settings
--/Account(default)
--/Help
--/About
I would like it in such a way: when I'm in any one of the sub pages under settings, and the browser go back in history, it should always go to the (default)Account page. Going back again will follow the normal browser history behavior.
What's the best way of handling it?
You can use history:
import createHistory from "history/createBrowserHistory"
const history = createHistory()
history.push("/account")
<Router history={history}/>
After quite some research, it seems there's no easy way of doing it, discussions in react-router github wouldn't disagree. A workaround seems to be the consensus. Which is a bit raw to my liking, but anything that works is good.
So instead of keeping every sub page under settings with their own route(to be handled by react-router), I have them controlled by state in settings component. And the following event handling is used:
componentDidMount = () => {
window.onpopstate= () => {
if(this.state.currentSubPage !== 'account-settings') {
this.props.history.push('/settings');
} else {
window.onpopstate = null;
}
};
}