Repository: guardian/facia-tool
Pull request #1806: Add tools audit pixel tracking
Author: login: waisingyiu
Labels: name: Seen-on-PROD
Created: 2025-04-29T17:09:30Z | Merged: 2025-05-06T08:45:00Z

## What's changed?
<!-- Detail the main feature of this PR and optionally a link to the relevant issue / card -->

This PR adds the [Tools Audit tracking pixel](https://github.com/guardian/editorial-tools-user-telemetry-service?tab=readme-ov-file#tracking-pixel) to allow us to understand more how the facia tool is being used by different users. The results will be visible in the [tools audit dashboard](https://metrics.gutools.co.uk/d/aegzv59r9eghsd/tool-audit?orgId=1&var-domain=birthdays.gutools.co.uk&var-path=All&var-interval=1d&var-chosen_apps=All).

## Implementation notes

We aim to add pixel tracking to all pages that can be served to authenticated users.  

Please note that some of the pages are changed via client-side JS so we cannot rely completely on the browser to send the tracking data.  We added a new hook on top of `useLocation` to send the tracking data when the location path has changed.  

This `useLocation` is available for react-router 5.1.0 or above so we also did a version bump on it.  Despite its major version change, it is [a minor release](https://github.com/remix-run/react-router/releases/tag/v5.0.0).

The changes have been validated on CODE.  I could see the requests to the telemetry service as a result of the pixel tracking when I loaded the fronts tool, the config tool and troubleshoot page.

| Requests |
| --- |
| <img width="640px" src="https://github.com/user-attachments/assets/858874d9-0187-4209-9353-da59aee329a4" /> |
| <img width="640px" src="https://github.com/user-attachments/assets/260857d5-47c3-4616-a0bd-80348a163d28" /> |
| <img width="640px" src="https://github.com/user-attachments/assets/02972047-c761-474c-8ab9-e4839a7a88b3" /> |

## Checklist

### General
- [ ] 🤖 Relevant tests added
- [X] ✅ CI checks / tests run locally
- [X] 🔍 Checked on CODE

### Client
- [X] 🚫 No obvious console errors on the client (i.e. React dev mode errors)
- [X] 🎛️ No regressions with existing user interactions (i.e. all existing buttons, inputs etc. work)
- [ ] 📷 Screenshots / GIFs of relevant UI changes included



Diff:
diff --git a/app/controllers/BaseFaciaController.scala b/app/controllers/BaseFaciaController.scala
index 400d3594e31..21a47a94c7a 100644
--- a/app/controllers/BaseFaciaController.scala
+++ b/app/controllers/BaseFaciaController.scala
@@ -130,4 +130,7 @@ abstract class BaseFaciaController(deps: BaseFaciaControllerComponents)
       s"${claimedAuth.user.email} is not valid for use with the Fronts Tool. You need to use your Guardian Google account to login. Please sign in with your Guardian Google account first, then retry logging in."
 
   }
+
+  val telemetryUrl =
+    s"https://user-telemetry.${config.environment.correspondingToolsDomainSuffix}/guardian-tool-accessed?app=facia-tool&stage=${config.environment.stage.toUpperCase()}"
 }
diff --git a/app/controllers/DefaultsController.scala b/app/controllers/DefaultsController.scala
index a9ea43d1940..faa15bd0fbd 100644
--- a/app/controllers/DefaultsController.scala
+++ b/app/controllers/DefaultsController.scala
@@ -34,7 +34,8 @@ case class Defaults(
     capiLiveUrl: String = "",
     capiPreviewUrl: String = "",
     availableTerritories: Iterable[TargetedTerritory] = Nil,
-    availableTemplates: List[CuratedPlatformDefinition]
+    availableTemplates: List[CuratedPlatformDefinition],
+    telemetryUrl: String
 )
 
 class DefaultsController(
@@ -88,7 +89,8 @@ class DefaultsController(
             None,
             availableTerritories = TargetedTerritory.allTerritories,
             availableTemplates =
-              EditionsAppTemplates.getAvailableTemplates ++ FeastAppTemplates.getAvailableTemplates
+              EditionsAppTemplates.getAvailableTemplates ++ FeastAppTemplates.getAvailableTemplates,
+            telemetryUrl = telemetryUrl
           )
         )
       )
diff --git a/app/controllers/TroubleshootController.scala b/app/controllers/TroubleshootController.scala
index 2fd2b3f8f4a..b443850ebcb 100644
--- a/app/controllers/TroubleshootController.scala
+++ b/app/controllers/TroubleshootController.scala
@@ -4,8 +4,14 @@ import model.Cached
 
 class TroubleshootController(val deps: BaseFaciaControllerComponents)
     extends BaseFaciaController(deps) {
+
   def troubleshoot(section: String) = AccessAuthAction { request =>
     val identity = request.user
-    Cached(60) { Ok(views.html.troubleshoot(identity)) }
+    Cached(60) {
+      Ok(
+        views.html
+          .troubleshoot(identity, maybeTelemetryUrl = Some(telemetryUrl))
+      )
+    }
   }
 }
diff --git a/app/controllers/V2App.scala b/app/controllers/V2App.scala
index e50bedc344e..80e5afbf3cf 100644
--- a/app/controllers/V2App.scala
+++ b/app/controllers/V2App.scala
@@ -125,7 +125,8 @@ class V2App(
         routes.FaciaContentApiProxy.capiLive("").absoluteURL(true),
         routes.FaciaContentApiProxy.capiPreview("").absoluteURL(true),
         TargetedTerritory.allTerritories,
-        EditionsAppTemplates.getAvailableTemplates ++ FeastAppTemplates.getAvailableTemplates
+        EditionsAppTemplates.getAvailableTemplates ++ FeastAppTemplates.getAvailableTemplates,
+        telemetryUrl = telemetryUrl
       )
 
       Ok(
@@ -143,7 +144,8 @@ class V2App(
               )
             case _ =>
               None
-          }
+          },
+          maybeTelemetryUrl = Some(telemetryUrl)
         )
       )
     }
diff --git a/app/controllers/ViewsController.scala b/app/controllers/ViewsController.scala
index 27397a94a5b..d9de6fe241f 100644
--- a/app/controllers/ViewsController.scala
+++ b/app/controllers/ViewsController.scala
@@ -39,7 +39,13 @@ class ViewsController(
       val identity = request.user
       Cached(60) {
         Ok(
-          views.html.priority(Option(identity), config.facia.stage, isDev, true)
+          views.html.priority(
+            Option(identity),
+            config.facia.stage,
+            isDev,
+            true,
+            maybeTelemetryUrl = Some(telemetryUrl)
+          )
         )
       }
     }
@@ -59,7 +65,8 @@ class ViewsController(
               overrideIsDev(request, isDev),
               assetsManager.pathForCollections,
               priority != "email",
-              priority
+              priority,
+              maybeTelemetryUrl = Some(telemetryUrl)
             )
           )
         }
@@ -76,7 +83,8 @@ class ViewsController(
             config.facia.stage,
             overrideIsDev(request, isDev),
             assetsManager.pathForConfig,
-            false
+            false,
+            maybeTelemetryUrl = Some(telemetryUrl)
           )
         )
       }
diff --git a/app/views/V2App/app.scala.html b/app/views/V2App/app.scala.html
index bf21f25d8c7..41b1b2993d2 100644
--- a/app/views/V2App/app.scala.html
+++ b/app/views/V2App/app.scala.html
@@ -5,7 +5,8 @@
     faviconLocation: String,
     clientConfigJson: String,
     isDev: Boolean,
-    maybePinboardUrl: Option[String]
+    maybePinboardUrl: Option[String],
+	maybeTelemetryUrl: Option[String]
 )
 <!DOCTYPE html>
 <html lang="en">
@@ -52,11 +53,11 @@
             }
             window.$RefreshSig$ = () => (type) => type
             window.__vite_plugin_react_preamble_installed__ = true
-        </script>
+		</script>
         <script type="module" src="http://localhost:5173/assets/fronts-client-v2/@@vite/client"></script>
         <script type="module" src="http://localhost:5173/assets/fronts-client-v2/src/index.tsx"></script>
     } else {
-        <link rel="stylesheet" href="@cssLocation">
+		<link rel="stylesheet" href="@cssLocation">
         <script type="module" src="@jsLocation" type="application/javascript"></script>
     }
 
diff --git a/app/views/admin_main.scala.html b/app/views/admin_main.scala.html
index 1bcd09dfae9..e5425568143 100644
--- a/app/views/admin_main.scala.html
+++ b/app/views/admin_main.scala.html
@@ -1,4 +1,4 @@
-@(identity: Option[com.gu.pandomainauth.model.User] = None, stage: String, isDev: Boolean, bundlePath: String, displayV2Message: Boolean = false, priority: String = "")
+@(identity: Option[com.gu.pandomainauth.model.User] = None, stage: String, isDev: Boolean, bundlePath: String, displayV2Message: Boolean = false, priority: String = "", maybeTelemetryUrl: Option[String] = None)
 
 @import switchboard.SwitchManager
 
@@ -26,5 +26,7 @@
         @templates.vertical_layout()
         @templates.main()
     }
+
+	@templates.pixel_tracking(maybeTelemetryUrl)
     </body>
 </html>
diff --git a/app/views/priority.scala.html b/app/views/priority.scala.html
index 1f6884ed3ca..c9fb1302aa6 100644
--- a/app/views/priority.scala.html
+++ b/app/views/priority.scala.html
@@ -1,4 +1,4 @@
-@(identity: Option[com.gu.pandomainauth.model.User] = None, stage: String, isDev: Boolean, displayV2Message: Boolean = false, priority: String = "")
+@(identity: Option[com.gu.pandomainauth.model.User] = None, stage: String, isDev: Boolean, displayV2Message: Boolean = false, priority: String = "", maybeTelemetryUrl: Option[String] = None)
 
 @import switchboard.SwitchManager
 
@@ -36,5 +36,7 @@
             </div>
         </main>
     }
+
+	@templates.pixel_tracking(maybeTelemetryUrl)
     </body>
 </html>
diff --git a/app/views/templates/pixel_tracking.scala.html b/app/views/templates/pixel_tracking.scala.html
new file mode 100644
index 00000000000..741b1aacd04
--- /dev/null
+++ b/app/views/templates/pixel_tracking.scala.html
@@ -0,0 +1,9 @@
+@(maybeTelemetryUrl: Option[String])
+
+@maybeTelemetryUrl.map { telemetryUrl => {
+  <script type="module">
+    const image = new Image();
+    image.src = `@Html(telemetryUrl)&path=${window.location.pathname}`;
+  </script>
+  }
+}
diff --git a/app/views/troubleshoot.scala.html b/app/views/troubleshoot.scala.html
index a687442b3da..ca810d2bb60 100644
--- a/app/views/troubleshoot.scala.html
+++ b/app/views/troubleshoot.scala.html
@@ -1,4 +1,4 @@
-@(identity: com.gu.pandomainauth.model.User)
+@(identity: com.gu.pandomainauth.model.User, maybeTelemetryUrl: Option[String] = None)
 <!doctype html>
 <html lang="en">
 <head>
@@ -38,5 +38,7 @@
         </nav>
 
         <main class="mainContainer">Have you tried turning it off and on again?</main>
+
+		@templates.pixel_tracking(maybeTelemetryUrl)
     </body>
 </html>
diff --git a/fronts-client/package.json b/fronts-client/package.json
index c59b4ec741b..c0eff7ffe42 100644
--- a/fronts-client/package.json
+++ b/fronts-client/package.json
@@ -99,8 +99,8 @@
 		"react-is": "16.8.0",
 		"react-modal": "^3.6.1",
 		"react-redux": "^8.0.5",
-		"react-router": "4.2.0",
-		"react-router-dom": "4.2.2",
+		"react-router": "5.1.0",
+		"react-router-dom": "5.1.0",
 		"react-router-redux": "^4.0.8",
 		"react-testing-library": "6.1.2",
 		"react-transition-group": "^2.6.0",
@@ -124,6 +124,6 @@
 	"resolutions": {
 		"chokidar": "^3.4.0",
 		"@types/react": "16.14.40",
-		"history": "4.7.2"
+		"history": "^4.7.2"
 	}
 }
diff --git a/fronts-client/src/components/App.tsx b/fronts-client/src/components/App.tsx
index 0c46f4a613c..7f50ffe49f3 100644
--- a/fronts-client/src/components/App.tsx
+++ b/fronts-client/src/components/App.tsx
@@ -40,6 +40,8 @@ import FeaturesView from './Features/FeaturesView';
 import { PlaceholderAnimation } from 'components/BasePlaceholder';
 import OptionsModal from './modals/OptionsModal';
 import BannerNotification from './notifications/BannerNotification';
+import pageConfig from 'util/extractConfigFromPage';
+import { useLocation } from 'react-router';
 
 // NB the properties described in font-face work as matchers, assigning text to the font imported by the source.
 // this is why we have 2 declarations of font-weight in several of these font-faces. Assigning either hits this font.
@@ -139,28 +141,41 @@ const BackgroundHeader = styled.div`
 	width: 100%;
 `;
 
-const App = () => (
-	<ThemeProvider theme={styleTheme}>
-		<DropDisabler>
-			<BannerNotification />
-			<AppContainer>
-				<BackgroundHeader>
-					<SectionHeaderWithLogo greyHeader={true} />
-				</BackgroundHeader>
-				<Switch>
-					<Route {...frontsEditPathProps} component={FrontsEdit} />
-					<Route {...issuePathProps} component={FrontsEdit} />
-					<Route {...frontsFeatureProps} component={FeaturesView} />
-					<Route exact path="/" component={Home} />
-					<Route exact path={manageEditions} component={ManageView} />
-					<Route component={NotFound} />
-				</Switch>
-			</AppContainer>
-			<PlaceholderAnimation />
-			<AppFonts />
-			<OptionsModal />
-		</DropDisabler>
-	</ThemeProvider>
-);
+function usePixelTracking() {
+	const location = useLocation();
+	React.useEffect(() => {
+		if (pageConfig.telemetryUrl) {
+			const image = new Image();
+			image.src = `${pageConfig.telemetryUrl}&path=${window.location.pathname}`;
+		}
+	}, [location]);
+}
+
+const App = () => {
+	usePixelTracking();
+	return (
+		<ThemeProvider theme={styleTheme}>
+			<DropDisabler>
+				<BannerNotification />
+				<AppContainer>
+					<BackgroundHeader>
+						<SectionHeaderWithLogo greyHeader={true} />
+					</BackgroundHeader>
+					<Switch>
+						<Route {...frontsEditPathProps} component={FrontsEdit} />
+						<Route {...issuePathProps} component={FrontsEdit} />
+						<Route {...frontsFeatureProps} component={FeaturesView} />
+						<Route exact path="/" component={Home} />
+						<Route exact path={manageEditions} component={ManageView} />
+						<Route component={NotFound} />
+					</Switch>
+				</AppContainer>
+				<PlaceholderAnimation />
+				<AppFonts />
+				<OptionsModal />
+			</DropDisabler>
+		</ThemeProvider>
+	);
+};
 
 export default App;
diff --git a/fronts-client/src/fixtures/initialState.ts b/fronts-client/src/fixtures/initialState.ts
index d56170b79eb..f955721c494 100644
--- a/fronts-client/src/fixtures/initialState.ts
+++ b/fronts-client/src/fixtures/initialState.ts
@@ -260,6 +260,7 @@ const config: Config = {
 	capiLiveUrl: 'https://fronts.local.dev-gutools.co.uk/api/live/',
 	capiPreviewUrl: 'https://fronts.local.dev-gutools.co.uk/api/preview/',
 	availableTemplates: [],
+	telemetryUrl: '',
 };
 
 const externalArticles = {
diff --git a/fronts-client/src/types/Config.ts b/fronts-client/src/types/Config.ts
index f9e4648d7d7..f1942fedeea 100644
--- a/fronts-client/src/types/Config.ts
+++ b/fronts-client/src/types/Config.ts
@@ -48,6 +48,7 @@ interface Config {
 		featureSwitches: FeatureSwitch[];
 	};
 	availableTemplates: EditionPriority[];
+	telemetryUrl: string;
 }
 
 export { Config };
diff --git a/fronts-client/yarn.lock b/fronts-client/yarn.lock
index 256f3560bce..ed829cea79a 100644
--- a/fronts-client/yarn.lock
+++ b/fronts-client/yarn.lock
@@ -4758,16 +4758,17 @@ highlight-es@^1.0.0:
     is-es2016-keyword "^1.0.0"
     js-tokens "^3.0.0"
 
-history@*, history@4.7.2, history@^4.7.2:
-  version "4.7.2"
-  resolved "https://registry.yarnpkg.com/history/-/history-4.7.2.tgz#22b5c7f31633c5b8021c7f4a8a954ac139ee8d5b"
-  integrity sha512-1zkBRWW6XweO0NBcjiphtVJVsIQ+SXF29z9DVkceeaSLVMFXHool+fdCZD4spDCfZJCILPILc3bm7Bc+HRi0nA==
+history@*, history@^4.7.2, history@^4.9.0:
+  version "4.10.1"
+  resolved "https://registry.yarnpkg.com/history/-/history-4.10.1.tgz#33371a65e3a83b267434e2b3f3b1b4c58aad4cf3"
+  integrity sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==
   dependencies:
-    invariant "^2.2.1"
+    "@babel/runtime" "^7.1.2"
     loose-envify "^1.2.0"
-    resolve-pathname "^2.2.0"
-    value-equal "^0.4.0"
-    warning "^3.0.0"
+    resolve-pathname "^3.0.0"
+    tiny-invariant "^1.0.2"
+    tiny-warning "^1.0.0"
+    value-equal "^1.0.1"
 
 history@^5.3.0:
   version "5.3.0"
@@ -4776,12 +4777,7 @@ history@^5.3.0:
   dependencies:
     "@babel/runtime" "^7.7.6"
 
-hoist-non-react-statics@^2.3.0, hoist-non-react-statics@^2.5.0:
-  version "2.5.5"
-  resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-2.5.5.tgz#c5903cf409c0dfd908f388e619d86b9c1174cb47"
-  integrity sha512-rqcy4pJo55FTTLWt+bU8ukscqHeE/e9KWvsOW2b/a3afxQZhwkQdT1rPPCJ0rYXdj4vNcasY8zHTH+jF/qStxw==
-
-hoist-non-react-statics@^3.0.0, hoist-non-react-statics@^3.2.1, hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.2:
+hoist-non-react-statics@^3.0.0, hoist-non-react-statics@^3.1.0, hoist-non-react-statics@^3.2.1, hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.2:
   version "3.3.2"
   resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45"
   integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==
@@ -4992,7 +4988,7 @@ internal-slot@^1.1.0:
   resolved "https://registry.yarnpkg.com/internmap/-/internmap-2.0.3.tgz#6685f23755e43c524e251d29cbc97248e3061009"
   integrity sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==
 
-invariant@^2.2.1, invariant@^2.2.2, invariant@^2.2.4:
+invariant@^2.2.4:
   version "2.2.4"
   resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6"
   integrity sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==
@@ -6204,6 +6200,14 @@ min-indent@^1.0.0:
   resolved "https://registry.yarnpkg.com/min-indent/-/min-indent-1.0.1.tgz#a63f681673b30571fbe8bc25686ae746eefa9869"
   integrity sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==
 
+mini-create-react-context@^0.3.0:
+  version "0.3.3"
+  resolved "https://registry.yarnpkg.com/mini-create-react-context/-/mini-create-react-context-0.3.3.tgz#b1b2bc6604d3a6c5d9752bad7692615410ebb38e"
+  integrity sha512-TtF6hZE59SGmS4U8529qB+jJFeW6asTLDIpPgvPLSCsooAwJS7QprHIFTqv9/Qh3NdLwQxFYgiHX5lqb6jqzPA==
+  dependencies:
+    "@babel/runtime" "^7.12.1"
+    tiny-warning "^1.0.3"
+
 minimalistic-assert@^1.0.0:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7"
@@ -6812,7 +6816,7 @@ prop-types-exact@^1.2.0:
     object.assign "^4.1.7"
     own-keys "^1.0.0"
 
-prop-types@^15.5.4, prop-types@^15.5.8, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2, prop-types@^15.8.1:
+prop-types@^15.5.8, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2, prop-types@^15.8.1:
   version "15.8.1"
   resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5"
   integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==
@@ -7131,7 +7135,7 @@ react-is@16.8.0:
   resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.8.0.tgz#518db476214f3fb0716af9f82dfd420225ae970f"
   integrity sha512-LOy+3La39aduxaPfuj+lCXC5RQ8ukjVPAAsFJ3yQ+DIOLf4eR9OMKeWKF0IzjRyE95xMj5QELwiXGgfQsIJguA==
 
-react-is@^16.13.1, react-is@^16.4.2, react-is@^16.7.0, react-is@^16.8.4, react-is@^16.8.6, react-is@^16.9.0:
+react-is@^16.13.1, react-is@^16.4.2, react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.4, react-is@^16.8.6, react-is@^16.9.0:
   version "16.13.1"
   resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
   integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
@@ -7210,48 +7214,39 @@ react-redux@^8.0.5:
     react-is "^18.0.0"
     use-sync-external-store "^1.0.0"
 
-react-router-dom@4.2.2:
-  version "4.2.2"
-  resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-4.2.2.tgz#c8a81df3adc58bba8a76782e946cbd4eae649b8d"
-  integrity sha512-cHMFC1ZoLDfEaMFoKTjN7fry/oczMgRt5BKfMAkTu5zEuJvUiPp1J8d0eXSVTnBh6pxlbdqDhozunOOLtmKfPA==
+react-router-dom@5.1.0:
+  version "5.1.0"
+  resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-5.1.0.tgz#48ad018d71fb7835212587e4c90bd2e3d2417e31"
+  integrity sha512-OkxKbMKjO7IkYqnoaZNX19MnwgjhxwZE871cPUTq0YU2wpIw7QwGxSnSoNRMOa7wO1TwvJJMFpgiEB4C/gVhTw==
   dependencies:
-    history "^4.7.2"
-    invariant "^2.2.2"
+    "@babel/runtime" "^7.1.2"
+    history "^4.9.0"
     loose-envify "^1.3.1"
-    prop-types "^15.5.4"
-    react-router "^4.2.0"
-    warning "^3.0.0"
+    prop-types "^15.6.2"
+    react-router "5.1.0"
+    tiny-invariant "^1.0.2"
+    tiny-warning "^1.0.0"
 
 react-router-redux@^4.0.8:
   version "4.0.8"
   resolved "https://registry.yarnpkg.com/react-router-redux/-/react-router-redux-4.0.8.tgz#227403596b5151e182377dab835b5d45f0f8054e"
   integrity sha512-lzlK+S6jZnn17BZbzBe6F8ok3YAhGAUlyWgRu3cz5mT199gKxfem5lNu3qcgzRiVhNEOFVG0/pdT+1t4aWhoQw==
 
-react-router@4.2.0:
-  version "4.2.0"
-  resolved "https://registry.yarnpkg.com/react-router/-/react-router-4.2.0.tgz#61f7b3e3770daeb24062dae3eedef1b054155986"
-  integrity sha512-DY6pjwRhdARE4TDw7XjxjZsbx9lKmIcyZoZ+SDO7SBJ1KUeWNxT22Kara2AC7u6/c2SYEHlEDLnzBCcNhLE8Vg==
-  dependencies:
-    history "^4.7.2"
-    hoist-non-react-statics "^2.3.0"
-    invariant "^2.2.2"
-    loose-envify "^1.3.1"
-    path-to-regexp "^1.7.0"
-    prop-types "^15.5.4"
-    warning "^3.0.0"
-
-react-router@^4.2.0:
-  version "4.3.1"
-  resolved "https://registry.yarnpkg.com/react-router/-/react-router-4.3.1.tgz#aada4aef14c809cb2e686b05cee4742234506c4e"
-  integrity sha512-yrvL8AogDh2X42Dt9iknk4wF4V8bWREPirFfS9gLU1huk6qK41sg7Z/1S81jjTrGHxa3B8R3J6xIkDAA6CVarg==
+react-router@5.1.0:
+  version "5.1.0"
+  resolved "https://registry.yarnpkg.com/react-router/-/react-router-5.1.0.tgz#739d0f3a57476363374e20d6e33e97f5ce2e00a3"
+  integrity sha512-n9HXxaL/6yRlig9XPfGyagI8+bUNdqcu7FUAx0/Z+Us22Z8iHsbkyJ21Inebn9HOxI5Nxlfc8GNabkNSeXfhqw==
   dependencies:
-    history "^4.7.2"
-    hoist-non-react-statics "^2.5.0"
-    invariant "^2.2.4"
+    "@babel/runtime" "^7.1.2"
+    history "^4.9.0"
+    hoist-non-react-statics "^3.1.0"
     loose-envify "^1.3.1"
+    mini-create-react-context "^0.3.0"
     path-to-regexp "^1.7.0"
-    prop-types "^15.6.1"
-    warning "^4.0.1"
+    prop-types "^15.6.2"
+    react-is "^16.6.0"
+    tiny-invariant "^1.0.2"
+    tiny-warning "^1.0.0"
 
 react-smooth@^4.0.0:
   version "4.0.4"
@@ -7585,10 +7580,10 @@ resolve-from@^5.0.0:
   resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69"
   integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==
 
-resolve-pathname@^2.2.0:
-  version "2.2.0"
-  resolved "https://registry.yarnpkg.com/resolve-pathname/-/resolve-pathname-2.2.0.tgz#7e9ae21ed815fd63ab189adeee64dc831eefa879"
-  integrity sha512-bAFz9ld18RzJfddgrO2e/0S2O81710++chRMUxHjXOYKF6jTAMrUNZrEZ1PvV0zlhfjidm08iRPdTLPno1FuRg==
+resolve-pathname@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/resolve-pathname/-/resolve-pathname-3.0.0.tgz#99d02224d3cf263689becbb393bc560313025dcd"
+  integrity sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng==
 
 resolve.exports@^2.0.0:
   version "2.0.3"
@@ -8413,11 +8408,16 @@ timed-out@4.0.1:
   resolved "https://registry.yarnpkg.com/timed-out/-/timed-out-4.0.1.tgz#f32eacac5a175bea25d7fab565ab3ed8741ef56f"
   integrity sha512-G7r3AhovYtr5YKOWQkta8RKAPb+J9IsO4uVmzjl8AZwfhs8UcUwTiD6gcJYSgOtzyjvQKrKYn41syHbUWMkafA==
 
-tiny-invariant@^1.0.4, tiny-invariant@^1.0.6, tiny-invariant@^1.3.1:
+tiny-invariant@^1.0.2, tiny-invariant@^1.0.4, tiny-invariant@^1.0.6, tiny-invariant@^1.3.1:
   version "1.3.3"
   resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.3.3.tgz#46680b7a873a0d5d10005995eb90a70d74d60127"
   integrity sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==
 
+tiny-warning@^1.0.0, tiny-warning@^1.0.3:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754"
+  integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==
+
 tmp@0.0.28:
   version "0.0.28"
   resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.28.tgz#172735b7f614ea7af39664fa84cf0de4e515d120"
@@ -8815,10 +8815,10 @@ v8-to-istanbul@^9.0.1:
     "@types/istanbul-lib-coverage" "^2.0.1"
     convert-source-map "^2.0.0"
 
-value-equal@^0.4.0:
-  version "0.4.0"
-  resolved "https://registry.yarnpkg.com/value-equal/-/value-equal-0.4.0.tgz#c5bdd2f54ee093c04839d71ce2e4758a6890abc7"
-  integrity sha512-x+cYdNnaA3CxvMaTX0INdTCN8m8aF2uY9BvEqmxuYp8bL09cs/kWVQPVGcA35fMktdOsP69IgU7wFj/61dJHEw==
+value-equal@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/value-equal/-/value-equal-1.0.1.tgz#1e0b794c734c5c0cade179c437d356d931a34d6c"
+  integrity sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw==
 
 vary@~1.1.2:
   version "1.1.2"
@@ -8889,14 +8889,7 @@ walker@^1.0.8:
   dependencies:
     makeerror "1.0.12"
 
-warning@^3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/warning/-/warning-3.0.0.tgz#32e5377cb572de4ab04753bdf8821c01ed605b7c"
-  integrity sha512-jMBt6pUrKn5I+OGgtQ4YZLdhIeJmObddh6CsibPxyQ5yPZm1XExSyzC1LCNX7BzhxWgiHmizBWJTHJIjMjTQYQ==
-  dependencies:
-    loose-envify "^1.0.0"
-
-warning@^4.0.1, warning@^4.0.3:
+warning@^4.0.3:
   version "4.0.3"
   resolved "https://registry.yarnpkg.com/warning/-/warning-4.0.3.tgz#16e9e077eb8a86d6af7d64aa1e05fd85b4678ca3"
   integrity sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==


Reviews:
user: login: SHession
body: Nice job! This looked like a tricky on to integrate the tracking into. 

I spotted one case where we the tracking pixel doesn't fire, when transitioning between pages i.e. navigating to https://fronts.code.dev-gutools.co.uk/v2/ -> [Any of the listed links]
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-30T10:56:06Z
user: login: waisingyiu
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-30T13:36:40Z
user: login: SHession
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-30T14:31:16Z
user: login: SHession
body: This looks like it's working nicely, just a couple of little things to address.

> The useLocation is available for react-router v5.1.0 or above, so I've upgraded it from v4.2.0 to this version. Despite the major ver bump, it is only [a minor release](https://github.com/remix-run/react-router/releases/tag/v5.0.0)

Haha, everyone makes mistakes! 

state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-01T09:45:38Z
user: login: waisingyiu
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-01T09:51:42Z
user: login: waisingyiu
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-01T12:26:45Z
user: login: dblatcher
body: I checked the changes made since Sam's reivew and can see the latest version works on CODE - LGTM 👍 
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-02T12:03:03Z
user: login: SHession
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-06T07:21:11Z

Review comments:
diff_hunk: @@ -36,5 +36,13 @@
             </div>
         </main>
     }
+
+	@maybeTelemetryUrl.map { telemetryUrl => {
+        <script type="module">
+			const image = new Image();
+			image.src = `@Html(telemetryUrl)&path=${window.location.pathname}`;
+		</script>
+		}
+	}
path: app/views/priority.scala.html
user: login: SHession
body: Would it be possible to move this into a reusable Twirl component to avoid the repetition? I'm also curious, do we need to the `type="module"` attribute? 
created_at: 2025-04-30T10:48:44Z
author_association: CONTRIBUTOR
diff_hunk: @@ -36,5 +36,13 @@
             </div>
         </main>
     }
+
+	@maybeTelemetryUrl.map { telemetryUrl => {
+        <script type="module">
+			const image = new Image();
+			image.src = `@Html(telemetryUrl)&path=${window.location.pathname}`;
+		</script>
+		}
+	}
path: app/views/priority.scala.html
user: login: waisingyiu
body: Great idea!  I've moved the snippet into a reusable twirl component in the next commit.

If we set the attribute `type="module"`, the code inside will be executed as a ES module and so the variables/constants created there will not be in the global scope.  I think it could minimize the risk of accidentally breaking the existing JS code (though it is highly unlikely there is really a constant `image` in the global scope).
created_at: 2025-04-30T13:36:40Z
author_association: CONTRIBUTOR
diff_hunk: @@ -36,5 +36,13 @@
             </div>
         </main>
     }
+
+	@maybeTelemetryUrl.map { telemetryUrl => {
+        <script type="module">
+			const image = new Image();
+			image.src = `@Html(telemetryUrl)&path=${window.location.pathname}`;
+		</script>
+		}
+	}
path: app/views/priority.scala.html
user: login: SHession
body: Good point, I hadn't thought of that
created_at: 2025-04-30T14:31:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -139,28 +141,44 @@ const BackgroundHeader = styled.div`
 	width: 100%;
 `;
 
-const App = () => (
-	<ThemeProvider theme={styleTheme}>
-		<DropDisabler>
-			<BannerNotification />
-			<AppContainer>
-				<BackgroundHeader>
-					<SectionHeaderWithLogo greyHeader={true} />
-				</BackgroundHeader>
-				<Switch>
-					<Route {...frontsEditPathProps} component={FrontsEdit} />
-					<Route {...issuePathProps} component={FrontsEdit} />
-					<Route {...frontsFeatureProps} component={FeaturesView} />
-					<Route exact path="/" component={Home} />
-					<Route exact path={manageEditions} component={ManageView} />
-					<Route component={NotFound} />
-				</Switch>
-			</AppContainer>
-			<PlaceholderAnimation />
-			<AppFonts />
-			<OptionsModal />
-		</DropDisabler>
-	</ThemeProvider>
-);
+function usePixelTracking() {
+	const location = useLocation();
+	React.useEffect(() => {
+		if (pageConfig.telemetryUrl) {
+			void fetch(`${pageConfig.telemetryUrl}&path=${location.pathname}`, {
+				credentials: 'include',
+			}).catch(() => {
+				console.error('Fail to send pixel tracking to telemetry service.');
+			});
path: fronts-client/src/components/App.tsx
user: login: SHession
body: Nothing wrong with making the call in this way, but making a request via an image element may be a simpler way to make a dumb GET call.
created_at: 2025-05-01T09:35:44Z
author_association: CONTRIBUTOR
diff_hunk: @@ -52,17 +53,19 @@
             }
             window.$RefreshSig$ = () => (type) => type
             window.__vite_plugin_react_preamble_installed__ = true
-        </script>
+		</script>
         <script type="module" src="http://localhost:5173/assets/fronts-client-v2/@@vite/client"></script>
         <script type="module" src="http://localhost:5173/assets/fronts-client-v2/src/index.tsx"></script>
     } else {
-        <link rel="stylesheet" href="@cssLocation">
+		<link rel="stylesheet" href="@cssLocation">
         <script type="module" src="@jsLocation" type="application/javascript"></script>
     }
 
     @maybePinboardUrl.map { pinboardUrl =>
         <script async defer src="@pinboardUrl"></script>
     }
+
+	@templates.pixel_tracking(maybeTelemetryUrl)
path: app/views/V2App/app.scala.html
user: login: SHession
body: I think we should be able to remove this component as the tracking will be fired by the client script, causing duplicate visit events. 
created_at: 2025-05-01T09:39:20Z
author_association: CONTRIBUTOR
diff_hunk: @@ -139,28 +141,44 @@ const BackgroundHeader = styled.div`
 	width: 100%;
 `;
 
-const App = () => (
-	<ThemeProvider theme={styleTheme}>
-		<DropDisabler>
-			<BannerNotification />
-			<AppContainer>
-				<BackgroundHeader>
-					<SectionHeaderWithLogo greyHeader={true} />
-				</BackgroundHeader>
-				<Switch>
-					<Route {...frontsEditPathProps} component={FrontsEdit} />
-					<Route {...issuePathProps} component={FrontsEdit} />
-					<Route {...frontsFeatureProps} component={FeaturesView} />
-					<Route exact path="/" component={Home} />
-					<Route exact path={manageEditions} component={ManageView} />
-					<Route component={NotFound} />
-				</Switch>
-			</AppContainer>
-			<PlaceholderAnimation />
-			<AppFonts />
-			<OptionsModal />
-		</DropDisabler>
-	</ThemeProvider>
-);
+function usePixelTracking() {
+	const location = useLocation();
+	React.useEffect(() => {
+		if (pageConfig.telemetryUrl) {
+			void fetch(`${pageConfig.telemetryUrl}&path=${location.pathname}`, {
path: fronts-client/src/components/App.tsx
user: login: SHession
body: It looks like `location.pathname` return the relative path, meaning in the case we are dropping the `/v2`:

<img width="622" alt="image" src="https://github.com/user-attachments/assets/7e4d1dea-b080-4d4c-96c7-b3e6e4e84d55" />

I expect if we were to call `window.location.pathname` here it would work as we would hope.



created_at: 2025-05-01T09:41:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -52,17 +53,19 @@
             }
             window.$RefreshSig$ = () => (type) => type
             window.__vite_plugin_react_preamble_installed__ = true
-        </script>
+		</script>
         <script type="module" src="http://localhost:5173/assets/fronts-client-v2/@@vite/client"></script>
         <script type="module" src="http://localhost:5173/assets/fronts-client-v2/src/index.tsx"></script>
     } else {
-        <link rel="stylesheet" href="@cssLocation">
+		<link rel="stylesheet" href="@cssLocation">
         <script type="module" src="@jsLocation" type="application/javascript"></script>
     }
 
     @maybePinboardUrl.map { pinboardUrl =>
         <script async defer src="@pinboardUrl"></script>
     }
+
+	@templates.pixel_tracking(maybeTelemetryUrl)
path: app/views/V2App/app.scala.html
user: login: waisingyiu
body: A great spot!
created_at: 2025-05-01T09:51:41Z
author_association: CONTRIBUTOR
diff_hunk: @@ -139,28 +141,44 @@ const BackgroundHeader = styled.div`
 	width: 100%;
 `;
 
-const App = () => (
-	<ThemeProvider theme={styleTheme}>
-		<DropDisabler>
-			<BannerNotification />
-			<AppContainer>
-				<BackgroundHeader>
-					<SectionHeaderWithLogo greyHeader={true} />
-				</BackgroundHeader>
-				<Switch>
-					<Route {...frontsEditPathProps} component={FrontsEdit} />
-					<Route {...issuePathProps} component={FrontsEdit} />
-					<Route {...frontsFeatureProps} component={FeaturesView} />
-					<Route exact path="/" component={Home} />
-					<Route exact path={manageEditions} component={ManageView} />
-					<Route component={NotFound} />
-				</Switch>
-			</AppContainer>
-			<PlaceholderAnimation />
-			<AppFonts />
-			<OptionsModal />
-		</DropDisabler>
-	</ThemeProvider>
-);
+function usePixelTracking() {
+	const location = useLocation();
+	React.useEffect(() => {
+		if (pageConfig.telemetryUrl) {
+			void fetch(`${pageConfig.telemetryUrl}&path=${location.pathname}`, {
path: fronts-client/src/components/App.tsx
user: login: waisingyiu
body: Yeah, `window.location.pathname` works for us.  You are right that we need to use `useLocation` hook here.
created_at: 2025-05-01T12:26:44Z
author_association: CONTRIBUTOR

PR comments:
user: login: github-actions[bot]
created_at: 2025-04-29T17:12:53Z
body: ### [Deploy build 9068 of `cms-fronts::facia-tool` to CODE](https://riffraff.gutools.co.uk/deployment/deployAgain?project=cms-fronts%3A%3Afacia-tool&build=9068&stage=CODE&updateStrategy=MostlyHarmless&action=deploy)
<details>
<summary>All deployment options</summary>

- [Deploy build 9068 of `cms-fronts::facia-tool` to CODE](https://riffraff.gutools.co.uk/deployment/deployAgain?project=cms-fronts%3A%3Afacia-tool&build=9068&stage=CODE&updateStrategy=MostlyHarmless&action=deploy)
- [Deploy parts of build 9068 to CODE by previewing it first](https://riffraff.gutools.co.uk/preview/yaml?project=cms-fronts%3A%3Afacia-tool&build=9068&stage=CODE&updateStrategy=MostlyHarmless)
- [What's on CODE right now?](https://riffraff.gutools.co.uk/deployment/history?projectName=cms-fronts%3A%3Afacia-tool&stage=CODE)
</details>

---
_From [guardian/actions-riff-raff](https://github.com/guardian/actions-riff-raff)._
<!-- guardian/actions-riff-raff for cms-fronts::facia-tool -->
author_association: NONE
user: login: waisingyiu
created_at: 2025-04-30T13:44:37Z
body: > Nice job! This looked like a tricky on to integrate the tracking into.
> 
> I spotted one case where we the tracking pixel doesn't fire, when transitioning between pages i.e. navigating to https://fronts.code.dev-gutools.co.uk/v2/ -> [Any of the listed links]

Yeah @SHession I've noticed it too.  It appears that clicking those links on the `v2` homepage does not cause page reload.  Apparently, it is handled by the [client side JS](https://github.com/guardian/facia-tool/blob/main/fronts-client/src/components/App.tsx#L151-L156) so the browser does not run the pixel tracking code again, as mentioned in the implementation notes.

Any suggestions are great.
author_association: CONTRIBUTOR
user: login: SHession
created_at: 2025-04-30T14:53:24Z
body: > Yeah @SHession I've noticed it too. It appears that clicking those links on the `v2` homepage does not cause page reload. Apparently, it is handled by the [client side JS](https://github.com/guardian/facia-tool/blob/main/fronts-client/src/components/App.tsx#L151-L156) so the browser does not run the pixel tracking code again, as mentioned in the implementation notes.
> 
> Any suggestions are great.

Sorry I missed your comment in the description. I've not very familiar with the fronts tool, but it looks we are using React router here: 

https://github.com/guardian/facia-tool/blob/309162a28472478d0b264a71dca7bedd236d6575/fronts-client/src/index.tsx#L82-L91

I wonder if we might be able to add a hook to App component which requests the tracking pixel when the location changes, similar to this example:

https://v5.reactrouter.com/web/api/Hooks/uselocation

Let me know what you think or if it's worth pairing. 





author_association: CONTRIBUTOR
user: login: waisingyiu
created_at: 2025-05-01T08:31:29Z
body: > > Yeah @SHession I've noticed it too. It appears that clicking those links on the `v2` homepage does not cause page reload. Apparently, it is handled by the [client side JS](https://github.com/guardian/facia-tool/blob/main/fronts-client/src/components/App.tsx#L151-L156) so the browser does not run the pixel tracking code again, as mentioned in the implementation notes.
> > Any suggestions are great.
> 
> Sorry I missed your comment in the description. I've not very familiar with the fronts tool, but it looks we are using React router here:
> 
> https://github.com/guardian/facia-tool/blob/309162a28472478d0b264a71dca7bedd236d6575/fronts-client/src/index.tsx#L82-L91
> 
> I wonder if we might be able to add a hook to App component which requests the tracking pixel when the location changes, similar to this example:
> 
> https://v5.reactrouter.com/web/api/Hooks/uselocation
> 
> Let me know what you think or if it's worth pairing.

Hey @SHession that's a great suggestion, and it can give us very useful insights on how the tool is used.  

The `useLocation` is available for react-router v5.1.0 or above, so I've upgraded it from v4.2.0 to this version.  Despite the major ver bump, it is only [a minor release](https://github.com/remix-run/react-router/releases/tag/v5.0.0)
author_association: CONTRIBUTOR
user: login: prout-bot
created_at: 2025-05-06T09:14:45Z
body: Seen on [PROD](https://fronts.gutools.co.uk/_healthcheck) (merged by @waisingyiu 29 minutes and 45 seconds ago) Please check your changes!
author_association: NONE