diff --git a/.changelog/1327.feature.md b/.changelog/1327.feature.md new file mode 100644 index 000000000..e65ff7892 --- /dev/null +++ b/.changelog/1327.feature.md @@ -0,0 +1 @@ +Integrate matomo analytics diff --git a/.eslintrc.js b/.eslintrc.js index 733cfcb76..77d6798bd 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -38,6 +38,15 @@ const config = { ], }, ], + 'no-restricted-syntax': [ + 'error', + { + selector: + 'MemberExpression[object.object.name="process"][object.property.name="env"][property.name="REACT_APP_ENABLE_OASIS_MATOMO_ANALYTICS"]', + message: + 'Replace with window.REACT_APP_ENABLE_OASIS_MATOMO_ANALYTICS to support overriding in e2e tests', + }, + ], 'prefer-template': 'error', 'react/jsx-no-target-blank': 'off', // Not needed with modern browsers diff --git a/playwright/tests/analytics.spec.ts b/playwright/tests/analytics.spec.ts new file mode 100644 index 000000000..a665b374f --- /dev/null +++ b/playwright/tests/analytics.spec.ts @@ -0,0 +1,94 @@ +import '../../src/types/global.d.ts' +import { Page, expect, test } from '@playwright/test' + +async function setup(page: Page, mode: 'allow-matomo-lib' | 'block-matomo-lib') { + await page.route('**/v1/', route => { + // Don't respond + }) + await page.route('https://matomo.oasis.io/matomo.php?**', route => { + // Don't send tracked events + }) + await page.route('https://matomo.oasis.io/matomo.js', async route => { + if (mode === 'allow-matomo-lib') route.continue() + if (mode === 'block-matomo-lib') route.abort('blockedbyclient') + }) + + const matomoRequests: string[] = [] + page.on('request', request => { + if (request.url().startsWith('https://matomo.oasis.io/')) matomoRequests.push(request.url()) + }) + + await page.goto('http://localhost:1234/mainnet/sapphire/block') + await expect(page.getByText('Latest Blocks')).toBeVisible() + await page.waitForTimeout(100) + + return { + getMatomoRequests: () => matomoRequests, + } +} + +test.describe('analytics', () => { + test('enabled', async ({ page }) => { + await page.addInitScript(() => (window.REACT_APP_ENABLE_OASIS_MATOMO_ANALYTICS = 'true')) + const { getMatomoRequests } = await setup(page, 'allow-matomo-lib') + await expect(page.getByText('tracking')).toBeVisible() + await expect(page.getByRole('button', { name: 'Privacy Settings' })).toBeVisible() + expect(getMatomoRequests()).toHaveLength(1) // Loaded library + + await page.getByRole('link', { name: 'Oasis Explorer' }).click() + await page.getByRole('button', { name: 'Decline' }).click() + expect(getMatomoRequests()).toHaveLength(1) + + await page.getByRole('button', { name: 'Privacy Settings' }).click() + await page.getByRole('button', { name: 'Accept' }).click() + await page.waitForTimeout(1) + expect(getMatomoRequests()).toHaveLength(2) // Tracked + + await page.getByRole('link', { name: 'Blocks' }).first().click() + await page.waitForRequest('https://matomo.oasis.io/matomo.php?**') // Debounced + await page.waitForTimeout(1) + expect(getMatomoRequests()).toHaveLength(3) // Tracked + }) + + test('adblocked', async ({ page }) => { + await page.addInitScript(() => (window.REACT_APP_ENABLE_OASIS_MATOMO_ANALYTICS = 'true')) + const { getMatomoRequests } = await setup(page, 'block-matomo-lib') + await expect(page.getByText('tracking')).not.toBeVisible() + await expect(page.getByRole('button', { name: 'Privacy Settings' })).toBeVisible() + expect(getMatomoRequests()).toHaveLength(1) // Tried to load library + await page.getByRole('link', { name: 'Oasis Explorer' }).click() + await page.getByRole('button', { name: 'Privacy Settings' }).click() + await expect(page.getByText('tracking failed')).toBeVisible() + expect(getMatomoRequests()).toHaveLength(1) // No new requests + }) + + test('disabled in env', async ({ page }) => { + await page.addInitScript(() => (window.REACT_APP_ENABLE_OASIS_MATOMO_ANALYTICS = 'false')) + const { getMatomoRequests } = await setup(page, 'allow-matomo-lib') + await expect(page.getByText('tracking')).not.toBeVisible() + await expect(page.getByRole('button', { name: 'Privacy Settings' })).not.toBeVisible() + expect(getMatomoRequests()).toHaveLength(0) + await page.getByRole('link', { name: 'Oasis Explorer' }).click() + expect(getMatomoRequests()).toHaveLength(0) + }) + + test('pre-consented and then disabled in env', async ({ page }) => { + await page.addInitScript(() => (window.REACT_APP_ENABLE_OASIS_MATOMO_ANALYTICS = 'true')) + const { getMatomoRequests } = await setup(page, 'allow-matomo-lib') + await test.step('consent', async () => { + expect(getMatomoRequests()).toHaveLength(1) // Loaded library + await page.getByRole('button', { name: 'Accept' }).click() + await page.waitForTimeout(1) + expect(getMatomoRequests()).toHaveLength(2) // Tracked + }) + + await page.addInitScript(() => (window.REACT_APP_ENABLE_OASIS_MATOMO_ANALYTICS = 'false')) + await page.goto('http://localhost:1234/mainnet/sapphire/block') + await page.waitForTimeout(100) + await expect(page.getByText('tracking')).not.toBeVisible() + await expect(page.getByRole('button', { name: 'Privacy Settings' })).not.toBeVisible() + expect(getMatomoRequests()).toHaveLength(2) // No new requests + await page.getByRole('link', { name: 'Oasis Explorer' }).click() + expect(getMatomoRequests()).toHaveLength(2) // No new requests + }) +}) diff --git a/public/index.html b/public/index.html index ffcea00c6..c91632d22 100644 --- a/public/index.html +++ b/public/index.html @@ -18,6 +18,10 @@
+ diff --git a/src/app/components/AnalyticsConsent/index.tsx b/src/app/components/AnalyticsConsent/index.tsx index 4ba791f3a..a88cb7de5 100644 --- a/src/app/components/AnalyticsConsent/index.tsx +++ b/src/app/components/AnalyticsConsent/index.tsx @@ -1,4 +1,4 @@ -/* eslint-disable react-hooks/rules-of-hooks -- REACT_APP_ENABLE_OASIS_MATOMO_ANALYTICS can't change in runtime */ +/* eslint-disable react-hooks/rules-of-hooks -- REACT_APP_ENABLE_OASIS_MATOMO_ANALYTICS won't change in runtime */ import { createContext, useContext, useEffect, useState } from 'react' import { useLocation } from 'react-router-dom' import { styled } from '@mui/material/styles' @@ -17,7 +17,7 @@ const AnalyticsContext = createContext<{ } | null>(null) export const AnalyticsConsentProvider = (props: { children: React.ReactNode }) => { - if (process.env.REACT_APP_ENABLE_OASIS_MATOMO_ANALYTICS !== 'true') return <>{props.children} + if (window.REACT_APP_ENABLE_OASIS_MATOMO_ANALYTICS !== 'true') return <>{props.children} const [hasAccepted, setHasAccepted] = useState< matomo.HasAccepted | 'loading' | 'timed_out_matomo_not_loaded_force_open' @@ -87,7 +87,7 @@ export const AnalyticsConsentProvider = (props: { children: React.ReactNode }) = } export const ReopenAnalyticsConsentButton = () => { - if (process.env.REACT_APP_ENABLE_OASIS_MATOMO_ANALYTICS !== 'true') return <> + if (window.REACT_APP_ENABLE_OASIS_MATOMO_ANALYTICS !== 'true') return <> const { t } = useTranslation() const context = useContext(AnalyticsContext) diff --git a/src/types/global.d.ts b/src/types/global.d.ts index 4ceea863a..e0c3cbc81 100644 --- a/src/types/global.d.ts +++ b/src/types/global.d.ts @@ -5,7 +5,8 @@ declare global { REACT_APP_BUILD_DATETIME: string REACT_APP_BUILD_SHA: string REACT_APP_BUILD_VERSION: string - REACT_APP_ENABLE_OASIS_MATOMO_ANALYTICS: 'true' | 'false' + /** Access it through {@link window.REACT_APP_ENABLE_OASIS_MATOMO_ANALYTICS} instead, to support overriding it in e2e tests */ + REACT_APP_ENABLE_OASIS_MATOMO_ANALYTICS: never REACT_APP_API: string REACT_APP_TESTNET_API: string REACT_APP_SHOW_BUILD_BANNERS: 'true' | 'false' @@ -29,6 +30,10 @@ declare global { REACT_APP_SHOW_FIAT_VALUES: 'true' | 'false' } } + + interface Window { + REACT_APP_ENABLE_OASIS_MATOMO_ANALYTICS: 'true' | 'false' + } } export {}