From 23c60cfa45d0c01c2a710de9c6a644cd91d1b3f3 Mon Sep 17 00:00:00 2001 From: Justin Sun Date: Thu, 9 Feb 2023 00:32:20 +0800 Subject: [PATCH] feat(vercel): Add support for analytics (Audiences & Web Vitals) (#6148) * feat(intergration/vercel): add vercel analytics support * docs(intergration/vercel): add vercel analytics prop * docs(intergration/vercel): bump version to 3.1.0 * Update packages/integrations/vercel/README.md Co-authored-by: Sarah Rainsberger * docs(intergration/vercel): add file name for example * feat(intergration/vercel): convert analytics to ts and support in edge * docs(intergration/vercel): move file names to code blocks as comments * fix(intergration/vercel): remove unused import * feat(intergration/vercel): add analytics support to static mode * chore(intergration/vercel): revert version change * style(intergration/vercel): add a blank line after astro import * chore(intergration/vercel): generate file by changeset * Update .changeset/eighty-bobcats-deliver.md Co-authored-by: Chris Swithinbank * Update packages/integrations/vercel/README.md Co-authored-by: Chris Swithinbank * Update packages/integrations/vercel/src/analytics.ts Co-authored-by: Chris Swithinbank * chore(intergration/vercel): simplify analytics script --------- Co-authored-by: Sarah Rainsberger Co-authored-by: Nate Moore Co-authored-by: Chris Swithinbank --- .changeset/eighty-bobcats-deliver.md | 5 ++ packages/integrations/vercel/README.md | 23 ++++++- packages/integrations/vercel/package.json | 5 +- packages/integrations/vercel/src/analytics.ts | 64 +++++++++++++++++++ .../integrations/vercel/src/edge/adapter.ts | 12 +++- .../vercel/src/serverless/adapter.ts | 7 +- .../integrations/vercel/src/static/adapter.ts | 11 +++- pnpm-lock.yaml | 17 +++++ 8 files changed, 137 insertions(+), 7 deletions(-) create mode 100644 .changeset/eighty-bobcats-deliver.md create mode 100644 packages/integrations/vercel/src/analytics.ts diff --git a/.changeset/eighty-bobcats-deliver.md b/.changeset/eighty-bobcats-deliver.md new file mode 100644 index 000000000000..064719c780ad --- /dev/null +++ b/.changeset/eighty-bobcats-deliver.md @@ -0,0 +1,5 @@ +--- +'@astrojs/vercel': minor +--- + +Add vercel analytics support diff --git a/packages/integrations/vercel/README.md b/packages/integrations/vercel/README.md index db89064c8775..64bd66a89cd0 100644 --- a/packages/integrations/vercel/README.md +++ b/packages/integrations/vercel/README.md @@ -87,6 +87,26 @@ vercel deploy --prebuilt To configure this adapter, pass an object to the `vercel()` function call in `astro.config.mjs`: +### analytics + +> **Type:** `boolean` +> **Available for:** Serverless, Edge, Static + +You can enable [Vercel Analytics](https://vercel.com/analytics) (including Web Vitals and Audiences) by setting `analytics: true`. This will inject Vercel’s tracking scripts into all your pages. + +```js +// astro.config.mjs +import { defineConfig } from 'astro/config'; +import vercel from '@astrojs/vercel/serverless'; + +export default defineConfig({ + output: 'server', + adapter: vercel({ + analytics: true + }) +}); +``` + ### includeFiles > **Type:** `string[]` @@ -95,6 +115,7 @@ To configure this adapter, pass an object to the `vercel()` function call in `as Use this property to force files to be bundled with your function. This is helpful when you notice missing files. ```js +// astro.config.mjs import { defineConfig } from 'astro/config'; import vercel from '@astrojs/vercel/serverless'; @@ -109,7 +130,6 @@ export default defineConfig({ > **Note** > When building for the Edge, all the dependencies get bundled in a single file to save space. **No extra file will be bundled**. So, if you _need_ some file inside the function, you have to specify it in `includeFiles`. - ### excludeFiles > **Type:** `string[]` @@ -118,6 +138,7 @@ export default defineConfig({ Use this property to exclude any files from the bundling process that would otherwise be included. ```js +// astro.config.mjs import { defineConfig } from 'astro/config'; import vercel from '@astrojs/vercel/serverless'; diff --git a/packages/integrations/vercel/package.json b/packages/integrations/vercel/package.json index 95908173b512..8332c64dd5e1 100644 --- a/packages/integrations/vercel/package.json +++ b/packages/integrations/vercel/package.json @@ -22,6 +22,7 @@ "./serverless": "./dist/serverless/adapter.js", "./serverless/entrypoint": "./dist/serverless/entrypoint.js", "./static": "./dist/static/adapter.js", + "./analytics": "./dist/analytics.js", "./package.json": "./package.json" }, "typesVersions": { @@ -45,9 +46,11 @@ }, "dependencies": { "@astrojs/webapi": "^2.0.0", + "@vercel/analytics": "^0.1.8", "@vercel/nft": "^0.22.1", "fast-glob": "^3.2.11", - "set-cookie-parser": "^2.5.1" + "set-cookie-parser": "^2.5.1", + "web-vitals": "^3.1.1" }, "peerDependencies": { "astro": "workspace:^2.0.8" diff --git a/packages/integrations/vercel/src/analytics.ts b/packages/integrations/vercel/src/analytics.ts new file mode 100644 index 000000000000..95dee83e37a7 --- /dev/null +++ b/packages/integrations/vercel/src/analytics.ts @@ -0,0 +1,64 @@ +import { inject } from '@vercel/analytics'; +import type { Metric } from 'web-vitals'; +import { getCLS, getFCP, getFID, getLCP, getTTFB } from 'web-vitals'; + +const vitalsUrl = 'https://vitals.vercel-analytics.com/v1/vitals'; + +type Options = { path: string; analyticsId: string }; + +const getConnectionSpeed = () => { + return 'connection' in navigator && + navigator['connection'] && + 'effectiveType' in (navigator['connection'] as unknown as { effectiveType: string }) + ? (navigator['connection'] as unknown as { effectiveType: string })['effectiveType'] + : ''; +}; + +const sendToAnalytics = (metric: Metric, options: Options) => { + const body = { + dsn: options.analyticsId, + id: metric.id, + page: options.path, + href: location.href, + event_name: metric.name, + value: metric.value.toString(), + speed: getConnectionSpeed(), + }; + const blob = new Blob([new URLSearchParams(body).toString()], { + type: 'application/x-www-form-urlencoded', + }); + if (navigator.sendBeacon) { + navigator.sendBeacon(vitalsUrl, blob); + } else + fetch(vitalsUrl, { + body: blob, + method: 'POST', + credentials: 'omit', + keepalive: true, + }); +}; + +function webVitals() { + const analyticsId = (import.meta as any).env.PUBLIC_VERCEL_ANALYTICS_ID; + if (!analyticsId) { + console.error('[Analytics] VERCEL_ANALYTICS_ID not found'); + return; + } + const options: Options = { path: window.location.pathname, analyticsId }; + try { + getFID((metric) => sendToAnalytics(metric, options)); + getTTFB((metric) => sendToAnalytics(metric, options)); + getLCP((metric) => sendToAnalytics(metric, options)); + getCLS((metric) => sendToAnalytics(metric, options)); + getFCP((metric) => sendToAnalytics(metric, options)); + } catch (err) { + console.error('[Analytics]', err); + } +} + +const mode = (import.meta as any).env.MODE as 'development' | 'production'; + +inject({ mode }); +if (mode === 'production') { + webVitals(); +} diff --git a/packages/integrations/vercel/src/edge/adapter.ts b/packages/integrations/vercel/src/edge/adapter.ts index 3f38a074e628..3e9eb5929cc8 100644 --- a/packages/integrations/vercel/src/edge/adapter.ts +++ b/packages/integrations/vercel/src/edge/adapter.ts @@ -1,4 +1,5 @@ import type { AstroAdapter, AstroConfig, AstroIntegration } from 'astro'; + import esbuild from 'esbuild'; import { relative as relativePath } from 'node:path'; import { fileURLToPath } from 'node:url'; @@ -24,9 +25,13 @@ function getAdapter(): AstroAdapter { export interface VercelEdgeConfig { includeFiles?: string[]; + analytics?: boolean; } -export default function vercelEdge({ includeFiles = [] }: VercelEdgeConfig = {}): AstroIntegration { +export default function vercelEdge({ + includeFiles = [], + analytics, +}: VercelEdgeConfig = {}): AstroIntegration { let _config: AstroConfig; let buildTempFolder: URL; let functionFolder: URL; @@ -35,7 +40,10 @@ export default function vercelEdge({ includeFiles = [] }: VercelEdgeConfig = {}) return { name: PACKAGE_NAME, hooks: { - 'astro:config:setup': ({ config, updateConfig }) => { + 'astro:config:setup': ({ config, updateConfig, injectScript }) => { + if (analytics) { + injectScript('page', 'import "@astrojs/vercel/analytics"'); + } const outDir = getVercelOutput(config.root); updateConfig({ outDir, diff --git a/packages/integrations/vercel/src/serverless/adapter.ts b/packages/integrations/vercel/src/serverless/adapter.ts index 3ff5eb3e57a9..884510516327 100644 --- a/packages/integrations/vercel/src/serverless/adapter.ts +++ b/packages/integrations/vercel/src/serverless/adapter.ts @@ -19,11 +19,13 @@ function getAdapter(): AstroAdapter { export interface VercelServerlessConfig { includeFiles?: string[]; excludeFiles?: string[]; + analytics?: boolean; } export default function vercelServerless({ includeFiles, excludeFiles, + analytics, }: VercelServerlessConfig = {}): AstroIntegration { let _config: AstroConfig; let buildTempFolder: URL; @@ -33,7 +35,10 @@ export default function vercelServerless({ return { name: PACKAGE_NAME, hooks: { - 'astro:config:setup': ({ config, updateConfig }) => { + 'astro:config:setup': ({ config, updateConfig, injectScript }) => { + if (analytics) { + injectScript('page', 'import "@astrojs/vercel/analytics"'); + } const outDir = getVercelOutput(config.root); updateConfig({ outDir, diff --git a/packages/integrations/vercel/src/static/adapter.ts b/packages/integrations/vercel/src/static/adapter.ts index 597c93626d37..28da5d4da649 100644 --- a/packages/integrations/vercel/src/static/adapter.ts +++ b/packages/integrations/vercel/src/static/adapter.ts @@ -9,13 +9,20 @@ function getAdapter(): AstroAdapter { return { name: PACKAGE_NAME }; } -export default function vercelStatic(): AstroIntegration { +export interface VercelStaticConfig { + analytics?: boolean; +} + +export default function vercelStatic({ analytics }: VercelStaticConfig = {}): AstroIntegration { let _config: AstroConfig; return { name: '@astrojs/vercel', hooks: { - 'astro:config:setup': ({ config }) => { + 'astro:config:setup': ({ config, injectScript }) => { + if (analytics) { + injectScript('page', 'import "@astrojs/vercel/analytics"'); + } config.outDir = new URL('./static/', getVercelOutput(config.root)); config.build.format = 'directory'; }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 73a3bfe828de..cd3ab9c87fb1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3335,6 +3335,7 @@ importers: specifiers: '@astrojs/webapi': ^2.0.0 '@types/set-cookie-parser': ^2.4.2 + '@vercel/analytics': ^0.1.8 '@vercel/nft': ^0.22.1 astro: workspace:* astro-scripts: workspace:* @@ -3342,11 +3343,14 @@ importers: fast-glob: ^3.2.11 mocha: ^9.2.2 set-cookie-parser: ^2.5.1 + web-vitals: ^3.1.1 dependencies: '@astrojs/webapi': link:../../webapi + '@vercel/analytics': 0.1.8 '@vercel/nft': 0.22.6 fast-glob: 3.2.12 set-cookie-parser: 2.5.1 + web-vitals: 3.1.1 devDependencies: '@types/set-cookie-parser': 2.4.2 astro: link:../../astro @@ -7623,6 +7627,15 @@ packages: '@unocss/scope': 0.15.6 dev: false + /@vercel/analytics/0.1.8: + resolution: {integrity: sha512-PQrOI8BJ9qUiVJuQfnKiJd15eDjDJH9TBKsNeMrtelT4NAk7d9mBVz1CoZkvoFnHQ0OW7Xnqmr1F2nScfAnznQ==} + peerDependencies: + react: ^16.8||^17||^18 + peerDependenciesMeta: + react: + optional: true + dev: false + /@vercel/nft/0.22.6: resolution: {integrity: sha512-gTsFnnT4mGxodr4AUlW3/urY+8JKKB452LwF3m477RFUJTAaDmcz2JqFuInzvdybYIeyIv1sSONEJxsxnbQ5JQ==} engines: {node: '>=14'} @@ -15398,6 +15411,10 @@ packages: resolution: {integrity: sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==} engines: {node: '>= 8'} + /web-vitals/3.1.1: + resolution: {integrity: sha512-qvllU+ZeQChqzBhZ1oyXmWsjJ8a2jHYpH8AMaVuf29yscOPZfTQTjQFRX6+eADTdsDE8IanOZ0cetweHMs8/2A==} + dev: false + /webidl-conversions/3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}