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 99e55da686ce..c402f547fa63 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.6" 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 fb87e5f4242a..96e383a2dee5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3313,6 +3313,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:* @@ -3320,11 +3321,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 @@ -7403,6 +7407,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'} @@ -15338,6 +15351,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==}