diff --git a/.circleci/config.yml b/.circleci/config.yml index 7e70954e6..8e6c0a2fe 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -7,6 +7,18 @@ executors: auth: username: $DOCKER_USER password: $DOCKER_PASS + + node_with_python: + docker: + - image: cimg/node:18.12.1 + auth: + username: $DOCKER_USER + password: $DOCKER_PASS + - image: cimg/python:3.9.7 + auth: + username: $DOCKER_USER + password: $DOCKER_PASS + python: docker: - image: cimg/python:3.9.7 @@ -43,20 +55,19 @@ orbs: jobs: lint: - executor: node + executor: node_with_python steps: - checkout - node/install-packages + - run: sudo apt-get update + - run: sudo apt-get install python3.10-venv + - run: npm run build-glean - run: npm run lint - run: cp .env-dist .env # Runs type checking: - run: npm run build deploy: - docker: - - image: docker:stable-git - auth: - username: $DOCKER_USER - password: $DOCKER_PASS + executor: node_with_python working_directory: /dockerflow steps: - checkout @@ -110,7 +121,7 @@ jobs: # is based on: # https://github.com/CircleCI-Public/heroku-orb/blob/master/src/jobs/deploy-via-git.yml # https://github.com/CircleCI-Public/heroku-orb/blob/master/src/commands/deploy-via-git.yml - executor: heroku/default + executor: python/default parameters: app-name: description: "The name of the Heroku App" diff --git a/.gitignore b/.gitignore index e536384c3..46c9927ce 100644 --- a/.gitignore +++ b/.gitignore @@ -63,3 +63,7 @@ next-env.d.ts # Sentry Auth Token .sentryclirc + +# Glean +.venv +/src/telemetry/generated/ diff --git a/.prettierignore b/.prettierignore index 7510a3c67..fe1badeef 100644 --- a/.prettierignore +++ b/.prettierignore @@ -20,3 +20,6 @@ src/middleware/ # These should be ignored anyway coverage/ playwright-report/ + +# Ignore Glean generated files +src/telemetry/generated/ diff --git a/Dockerfile b/Dockerfile index 84c406580..8e73395af 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,6 +4,12 @@ RUN addgroup -g 10001 app && \ adduser -D -G app -h /app -u 10001 app RUN rm -rf /tmp/* +# Install Python +ENV PYTHONUNBUFFERED=1 +RUN apk add --update --no-cache python3 && ln -sf python3 /usr/bin/python +RUN python3 -m ensurepip +RUN pip3 install --no-cache --upgrade pip setuptools + WORKDIR /app USER app @@ -18,7 +24,7 @@ RUN npm ci --audit=false && rm -rf ~app/.npm /tmp/* COPY .env-dist ./.env ARG S3_BUCKET ENV S3_BUCKET=$S3_BUCKET -RUN npm run build +RUN GLEAN_PYTHON=python GLEAN_PIP=pip npm run build ARG SENTRY_RELEASE ENV SENTRY_RELEASE=$SENTRY_RELEASE diff --git a/README.md b/README.md index 234064ea1..ee0191e04 100644 --- a/README.md +++ b/README.md @@ -80,6 +80,12 @@ We track commits that are largely style/formatting via `.git-blame-ignore-revs`. pip3 install -r .github/requirements.txt ``` +5. Generate required Glean files (needs re-ran anytime Glean `.yaml` files are updated): + + ```sh + npm run build-glean + ``` + ### Run 1. To run the server similar to production using a build phase, which includes minified and bundled assets: diff --git a/package-lock.json b/package-lock.json index 81413602e..de5b2c1f4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,6 +18,7 @@ "@google-cloud/pubsub": "^4.0.3", "@grpc/grpc-js": "1.8.21", "@leeoniya/ufuzzy": "^1.0.8", + "@mozilla/glean": "^2.0.2", "@sentry/nextjs": "^7.58.1", "@sentry/node": "^7.58.1", "@sentry/tracing": "^7.58.1", @@ -5543,6 +5544,29 @@ "react": ">=16" } }, + "node_modules/@mozilla/glean": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@mozilla/glean/-/glean-2.0.2.tgz", + "integrity": "sha512-ggboBpkNUraL+e3q+UtUMZmJ2ETS60/tiudIzqx8W1sZPZHRqiYf6eN64/NWr9pf7GfIvHXFwaisscTE/9UhdA==", + "dependencies": { + "fflate": "^0.8.0", + "jose": "^4.0.4", + "tslib": "^2.3.1", + "uuid": "^9.0.0" + }, + "bin": { + "glean": "dist/cli/cli.js" + }, + "engines": { + "node": ">=12.20.0", + "npm": ">=7.0.0" + } + }, + "node_modules/@mozilla/glean/node_modules/tslib": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.1.tgz", + "integrity": "sha512-t0hLfiEKfMUoqhG+U1oid7Pva4bbDPHYfJNiB7BiIjRkj1pyC++4N3huJfqY6aRH6VTB0rvtzQwjM4K6qpfOig==" + }, "node_modules/@ndelangen/get-tarball": { "version": "3.0.9", "resolved": "https://registry.npmjs.org/@ndelangen/get-tarball/-/get-tarball-3.0.9.tgz", @@ -20900,6 +20924,11 @@ "integrity": "sha512-3yurQZ2hD9VISAhJJP9bpYFNQrHHBXE2JxxjY5aLEcDi46RmAzJE2OC9FAde0yis5ElW0jTTzs0zfg/Cca4XqQ==", "dev": true }, + "node_modules/fflate": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.0.tgz", + "integrity": "sha512-FAdS4qMuFjsJj6XHbBaZeXOgaypXp8iw/Tpyuq/w3XA41jjLHT8NPA+n7czH/DDhdncq0nAyDZmPeWXh2qmdIg==" + }, "node_modules/file-entry-cache": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", @@ -37179,6 +37208,24 @@ "@types/react": ">=16" } }, + "@mozilla/glean": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@mozilla/glean/-/glean-2.0.2.tgz", + "integrity": "sha512-ggboBpkNUraL+e3q+UtUMZmJ2ETS60/tiudIzqx8W1sZPZHRqiYf6eN64/NWr9pf7GfIvHXFwaisscTE/9UhdA==", + "requires": { + "fflate": "^0.8.0", + "jose": "^4.0.4", + "tslib": "^2.3.1", + "uuid": "^9.0.0" + }, + "dependencies": { + "tslib": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.1.tgz", + "integrity": "sha512-t0hLfiEKfMUoqhG+U1oid7Pva4bbDPHYfJNiB7BiIjRkj1pyC++4N3huJfqY6aRH6VTB0rvtzQwjM4K6qpfOig==" + } + } + }, "@ndelangen/get-tarball": { "version": "3.0.9", "resolved": "https://registry.npmjs.org/@ndelangen/get-tarball/-/get-tarball-3.0.9.tgz", @@ -48706,6 +48753,11 @@ "integrity": "sha512-3yurQZ2hD9VISAhJJP9bpYFNQrHHBXE2JxxjY5aLEcDi46RmAzJE2OC9FAde0yis5ElW0jTTzs0zfg/Cca4XqQ==", "dev": true }, + "fflate": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.0.tgz", + "integrity": "sha512-FAdS4qMuFjsJj6XHbBaZeXOgaypXp8iw/Tpyuq/w3XA41jjLHT8NPA+n7czH/DDhdncq0nAyDZmPeWXh2qmdIg==" + }, "file-entry-cache": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", diff --git a/package.json b/package.json index deb485105..3a26dd7a3 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "type": "module", "scripts": { "dev": "next dev --port=6060", - "build": "npm run get-location-data && next build", + "build": "npm run get-location-data && npm run build-glean && next build", "start": "next start", "lint": "stylelint '**/*.scss' && prettier --check './src' && next lint --max-warnings=0", "fix": "prettier --write './src' && next lint --fix && stylelint --fix '**/*.scss'", @@ -24,7 +24,8 @@ "storybook": "storybook dev -p 6006", "build-storybook": "storybook build", "create-location-data": "node src/scripts/uploadAutoCompleteLocations.js", - "get-location-data": "node src/scripts/getAutoCompleteLocations.js" + "get-location-data": "node src/scripts/getAutoCompleteLocations.js", + "build-glean": "glean translate src/telemetry/metrics.yaml --format javascript --output src/telemetry/generated" }, "nodemonConfig": { "watch": [ @@ -55,6 +56,7 @@ "@google-cloud/pubsub": "^4.0.3", "@grpc/grpc-js": "1.8.21", "@leeoniya/ufuzzy": "^1.0.8", + "@mozilla/glean": "^2.0.2", "@sentry/nextjs": "^7.58.1", "@sentry/node": "^7.58.1", "@sentry/tracing": "^7.58.1", diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 000000000..e69de29bb diff --git a/src/app/(nextjs_migration)/(guest)/page.tsx b/src/app/(nextjs_migration)/(guest)/page.tsx index a1495f04b..b38758689 100644 --- a/src/app/(nextjs_migration)/(guest)/page.tsx +++ b/src/app/(nextjs_migration)/(guest)/page.tsx @@ -13,6 +13,7 @@ import LockImage from "../../../client/images/landing-lock@2x.webp"; import MailImage from "../../../client/images/landing-mail@2x.webp"; import NaturePhoneImage from "../../../client/images/landing-nature-phone@2x.webp"; import { getNonce } from "../functions/server/getNonce"; +import { PageLoadEvent } from "../components/client/PageLoadEvent"; export default function Home() { const l10n = getL10n(); @@ -34,6 +35,7 @@ export default function Home() { src="/nextjs_migration/client/js/landing.js" nonce={getNonce()} /> +

{l10n.getString("exposure-landing-hero-heading")}

diff --git a/src/app/(nextjs_migration)/components/client/PageLoadEvent.tsx b/src/app/(nextjs_migration)/components/client/PageLoadEvent.tsx new file mode 100644 index 000000000..4ff6bb2dd --- /dev/null +++ b/src/app/(nextjs_migration)/components/client/PageLoadEvent.tsx @@ -0,0 +1,23 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use client"; + +import { useEffect } from "react"; +import { usePathname } from "next/navigation"; +import { useGlean } from "../../../hooks/useGlean"; + +// Empty component that records a page view on first load. +export const PageLoadEvent = () => { + const { pageEvents } = useGlean(); + const pathname = usePathname(); + + // On first load of the page, record a page view. + useEffect(() => { + pageEvents.view.record({ path: pathname }); + }, [pageEvents.view, pathname]); + + // This component doesn't render anything. + return <>; +}; diff --git a/src/app/hooks/useGlean.ts b/src/app/hooks/useGlean.ts new file mode 100644 index 000000000..9976ebb5e --- /dev/null +++ b/src/app/hooks/useGlean.ts @@ -0,0 +1,37 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use client"; + +import { useEffect } from "react"; +import Glean from "@mozilla/glean/web"; +import * as pageEvents from "../../telemetry/generated/page"; + +// Custom hook that initializes Glean and returns the Glean objects required +// to record data. +export const useGlean = () => { + // Initialize Glean only on the first render + // of our custom hook. + useEffect(() => { + // Enable upload only if the user has not opted out of tracking. + const uploadEnabled = navigator.doNotTrack !== "1"; + + Glean.initialize("monitor.frontend", uploadEnabled, { + // This will submit an events ping every time an event is recorded. + maxEvents: 1, + }); + + // Glean debugging options can be found here: + // https://mozilla.github.io/glean/book/reference/debug/index.html + if (process.env.NEXT_PUBLIC_APP_ENV === "local") { + // Enable logging pings to the browser console. + Glean.setLogPings(true); + } + }, []); + + // Return all generated Glean objects required for recording data. + return { + pageEvents, + }; +}; diff --git a/src/middleware.ts b/src/middleware.ts index 8124956fd..e70179065 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -65,7 +65,7 @@ function generateCspData() { "script-src-attr 'none'", `connect-src 'self' ${ process.env.NODE_ENV === "development" ? "webpack://*" : "" - } https://*.google-analytics.com https://*.analytics.google.com https://*.googletagmanager.com https://*.ingest.sentry.io`, + } https://*.google-analytics.com https://*.analytics.google.com https://*.googletagmanager.com https://*.ingest.sentry.io https://incoming.telemetry.mozilla.org`, // `withSentryConfig` in next.config.js messes up the type, but we know that // it's a valid NextConfig with `images.remotePatterns` set: // eslint-disable-next-line @typescript-eslint/no-non-null-assertion diff --git a/src/telemetry/metrics.yaml b/src/telemetry/metrics.yaml new file mode 100644 index 000000000..2ed336a88 --- /dev/null +++ b/src/telemetry/metrics.yaml @@ -0,0 +1,28 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +# For more information on configuring this file: +# https://mozilla.github.io/glean/book/reference/yaml/metrics.html + +--- +$schema: moz://mozilla.org/schemas/glean/metrics/2-0-0 + +page: + view: + type: event + description: | + An event triggered when the page is loaded. + bugs: + - https://mozilla-hub.atlassian.net/browse/MNTOR-2022 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1848664#c6 + data_sensitivity: + - interaction + notification_emails: + - rhelmer@mozilla.com + expires: never + extra_keys: + path: + description: The path of the page. + type: string