[@astrojs/vercel] Individually enable Speed Insights and Web Analytics (#8021)

* Individually enable Speed Insights and Web Analytics

* Update pnpm-lock.yaml

* Remove .only on tests

* Fix build

* Move `beforeSend` out of config

* Address feedback from review

* Update README.md

* Add back the `analytics` property and add deprecation warning when used

* Add migration guide for the deprecated `analytics` property

* Update packages/integrations/vercel/README.md

Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca>

* Update README.md

* Fix external dependency issue

* Simplify plugin and reduce scope

* Update .changeset/sixty-teachers-tap.md

Co-authored-by: Emanuele Stoppa <my.burning@gmail.com>

* Apply feedback from review

* Move exposeEnv to speed-insights since it's only used there

---------

Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca>
Co-authored-by: Matthew Phillips <matthew@skypack.dev>
Co-authored-by: Emanuele Stoppa <my.burning@gmail.com>
This commit is contained in:
Chris 2023-09-14 14:02:11 +02:00 committed by GitHub
parent 7522bb4914
commit 2e8726feec
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 402 additions and 37 deletions

View file

@ -0,0 +1,26 @@
---
'@astrojs/vercel': minor
---
Enable Vercel Speed Insights and Vercel Web Analytics individually.
Deprecates the `analytics` property in `astro.config.mjs` in favor of `speedInsights` and `webAnalytics`.
If you're using the `analytics` property, you'll need to update your config to use the new properties:
```diff
// astro.config.mjs
export default defineConfig({
adapter: vercel({
- analytics: true,
+ webAnalytics: {
+ enabled: true
+ },
+ speedInsights: {
+ enabled: true
+ }
})
});
```
Allow configuration of Web Analytics with all available configuration options.
Bumps @vercel/analytics package to the latest version.

View file

@ -85,13 +85,13 @@ vercel deploy --prebuilt
To configure this adapter, pass an object to the `vercel()` function call in `astro.config.mjs`: To configure this adapter, pass an object to the `vercel()` function call in `astro.config.mjs`:
### analytics ### Web Analytics
**Type:** `boolean`<br> **Type:** `VercelWebAnalyticsConfig`<br>
**Available for:** Serverless, Static<br> **Available for:** Serverless, Edge, Static<br>
**Added in:** `@astrojs/vercel@3.1.0` **Added in:** `@astrojs/vercel@3.8.0`
You can enable [Vercel Analytics](https://vercel.com/analytics) (including Web Vitals and Audiences) by setting `analytics: true`. This will inject Vercels tracking scripts into all your pages. You can enable [Vercel Web Analytics](https://vercel.com/docs/concepts/analytics) by setting `webAnalytics: { enabled: true }`. This will inject Vercels tracking scripts into all of your pages.
```js ```js
// astro.config.mjs // astro.config.mjs
@ -101,7 +101,32 @@ import vercel from '@astrojs/vercel/serverless';
export default defineConfig({ export default defineConfig({
output: 'server', output: 'server',
adapter: vercel({ adapter: vercel({
analytics: true, webAnalytics: {
enabled: true,
},
}),
});
```
### Speed Insights
You can enable [Vercel Speed Insights](https://vercel.com/docs/concepts/speed-insights) by setting `speedInsights: { enabled: true }`. This will collect and send Web Vital data to Vercel.
**Type:** `VercelSpeedInsightsConfig`<br>
**Available for:** Serverless, Edge, Static<br>
**Added in:** `@astrojs/vercel@3.8.0`
```js
// astro.config.mjs
import { defineConfig } from 'astro/config';
import vercel from '@astrojs/vercel/serverless';
export default defineConfig({
output: 'server',
adapter: vercel({
speedInsights: {
enabled: true,
},
}), }),
}); });
``` ```

View file

@ -22,7 +22,7 @@
"./serverless": "./dist/serverless/adapter.js", "./serverless": "./dist/serverless/adapter.js",
"./serverless/entrypoint": "./dist/serverless/entrypoint.js", "./serverless/entrypoint": "./dist/serverless/entrypoint.js",
"./static": "./dist/static/adapter.js", "./static": "./dist/static/adapter.js",
"./analytics": "./dist/analytics.js", "./speed-insights": "./dist/speed-insights.js",
"./build-image-service": "./dist/image/build-service.js", "./build-image-service": "./dist/image/build-service.js",
"./dev-image-service": "./dist/image/dev-service.js", "./dev-image-service": "./dist/image/dev-service.js",
"./squoosh-dev-service": "./dist/image/squoosh-dev-service.js", "./squoosh-dev-service": "./dist/image/squoosh-dev-service.js",

View file

@ -1,3 +1,17 @@
export type VercelSpeedInsightsConfig = {
enabled: boolean;
};
export function getSpeedInsightsViteConfig(enabled?: boolean) {
if (enabled) {
return {
define: exposeEnv(['VERCEL_ANALYTICS_ID']),
};
}
return {};
}
/** /**
* While Vercel adds the `PUBLIC_` prefix for their `VERCEL_` env vars by default, some env vars * While Vercel adds the `PUBLIC_` prefix for their `VERCEL_` env vars by default, some env vars
* like `VERCEL_ANALYTICS_ID` aren't, so handle them here so that it works correctly in runtime. * like `VERCEL_ANALYTICS_ID` aren't, so handle them here so that it works correctly in runtime.

View file

@ -0,0 +1,30 @@
export type VercelWebAnalyticsConfig = {
enabled: boolean;
};
export async function getInjectableWebAnalyticsContent({
mode,
}: {
mode: 'development' | 'production';
}) {
const base = `window.va = window.va || function () { (window.vaq = window.vaq || []).push(arguments); };`;
if (mode === 'development') {
return `
${base}
var script = document.createElement('script');
script.defer = true;
script.src = 'https://cdn.vercel-insights.com/v1/script.debug.js';
var head = document.querySelector('head');
head.appendChild(script);
`;
}
return `${base}
var script = document.createElement('script');
script.defer = true;
script.src = '/_vercel/insights/script.js';
var head = document.querySelector('head');
head.appendChild(script);
`;
}

View file

@ -15,10 +15,17 @@ import {
type DevImageService, type DevImageService,
type VercelImageConfig, type VercelImageConfig,
} from '../image/shared.js'; } from '../image/shared.js';
import { exposeEnv } from '../lib/env.js';
import { getVercelOutput, removeDir, writeJson } from '../lib/fs.js'; import { getVercelOutput, removeDir, writeJson } from '../lib/fs.js';
import { copyDependenciesToFunction } from '../lib/nft.js'; import { copyDependenciesToFunction } from '../lib/nft.js';
import { getRedirects } from '../lib/redirects.js'; import { getRedirects } from '../lib/redirects.js';
import {
getSpeedInsightsViteConfig,
type VercelSpeedInsightsConfig,
} from '../lib/speed-insights.js';
import {
getInjectableWebAnalyticsContent,
type VercelWebAnalyticsConfig,
} from '../lib/web-analytics.js';
import { generateEdgeMiddleware } from './middleware.js'; import { generateEdgeMiddleware } from './middleware.js';
const PACKAGE_NAME = '@astrojs/vercel/serverless'; const PACKAGE_NAME = '@astrojs/vercel/serverless';
@ -64,9 +71,14 @@ function getAdapter({
} }
export interface VercelServerlessConfig { export interface VercelServerlessConfig {
/**
* @deprecated
*/
analytics?: boolean;
webAnalytics?: VercelWebAnalyticsConfig;
speedInsights?: VercelSpeedInsightsConfig;
includeFiles?: string[]; includeFiles?: string[];
excludeFiles?: string[]; excludeFiles?: string[];
analytics?: boolean;
imageService?: boolean; imageService?: boolean;
imagesConfig?: VercelImageConfig; imagesConfig?: VercelImageConfig;
devImageService?: DevImageService; devImageService?: DevImageService;
@ -75,9 +87,11 @@ export interface VercelServerlessConfig {
} }
export default function vercelServerless({ export default function vercelServerless({
analytics,
webAnalytics,
speedInsights,
includeFiles, includeFiles,
excludeFiles, excludeFiles,
analytics,
imageService, imageService,
imagesConfig, imagesConfig,
devImageService = 'sharp', devImageService = 'sharp',
@ -131,12 +145,25 @@ export default function vercelServerless({
return { return {
name: PACKAGE_NAME, name: PACKAGE_NAME,
hooks: { hooks: {
'astro:config:setup': ({ command, config, updateConfig, injectScript }) => { 'astro:config:setup': async ({ command, config, updateConfig, injectScript, logger }) => {
if (command === 'build' && analytics) { if (webAnalytics?.enabled || analytics) {
injectScript('page', 'import "@astrojs/vercel/analytics"'); if (analytics) {
logger.warn(
`The \`analytics\` property is deprecated. Please use the new \`webAnalytics\` and \`speedInsights\` properties instead.`
);
}
injectScript(
'head-inline',
await getInjectableWebAnalyticsContent({
mode: command === 'dev' ? 'development' : 'production',
})
);
}
if (command === 'build' && (speedInsights?.enabled || analytics)) {
injectScript('page', 'import "@astrojs/vercel/speed-insights"');
} }
const outDir = getVercelOutput(config.root); const outDir = getVercelOutput(config.root);
const viteDefine = exposeEnv(['VERCEL_ANALYTICS_ID']);
updateConfig({ updateConfig({
outDir, outDir,
build: { build: {
@ -145,7 +172,7 @@ export default function vercelServerless({
server: new URL('./dist/', config.root), server: new URL('./dist/', config.root),
}, },
vite: { vite: {
define: viteDefine, ...getSpeedInsightsViteConfig(speedInsights?.enabled || analytics),
ssr: { ssr: {
external: ['@vercel/nft'], external: ['@vercel/nft'],
}, },

View file

@ -1,8 +1,7 @@
import { inject } from '@vercel/analytics';
import type { Metric } from 'web-vitals'; import type { Metric } from 'web-vitals';
import { getCLS, getFCP, getFID, getLCP, getTTFB } from 'web-vitals'; import { onCLS, onFCP, onFID, onLCP, onTTFB } from 'web-vitals';
const vitalsUrl = 'https://vitals.vercel-analytics.com/v1/vitals'; const SPEED_INSIGHTS_INTAKE = 'https://vitals.vercel-analytics.com/v1/vitals';
type Options = { path: string; analyticsId: string }; type Options = { path: string; analyticsId: string };
@ -14,7 +13,7 @@ const getConnectionSpeed = () => {
: ''; : '';
}; };
const sendToAnalytics = (metric: Metric, options: Options) => { const sendToSpeedInsights = (metric: Metric, options: Options) => {
const body = { const body = {
dsn: options.analyticsId, dsn: options.analyticsId,
id: metric.id, id: metric.id,
@ -28,9 +27,9 @@ const sendToAnalytics = (metric: Metric, options: Options) => {
type: 'application/x-www-form-urlencoded', type: 'application/x-www-form-urlencoded',
}); });
if (navigator.sendBeacon) { if (navigator.sendBeacon) {
navigator.sendBeacon(vitalsUrl, blob); navigator.sendBeacon(SPEED_INSIGHTS_INTAKE, blob);
} else } else
fetch(vitalsUrl, { fetch(SPEED_INSIGHTS_INTAKE, {
body: blob, body: blob,
method: 'POST', method: 'POST',
credentials: 'omit', credentials: 'omit',
@ -38,27 +37,29 @@ const sendToAnalytics = (metric: Metric, options: Options) => {
}); });
}; };
function webVitals() { function collectWebVitals() {
const analyticsId = (import.meta as any).env.PUBLIC_VERCEL_ANALYTICS_ID; const analyticsId = (import.meta as any).env.PUBLIC_VERCEL_ANALYTICS_ID;
if (!analyticsId) { if (!analyticsId) {
console.error('[Analytics] VERCEL_ANALYTICS_ID not found'); console.error('[Speed Insights] VERCEL_ANALYTICS_ID not found');
return; return;
} }
const options: Options = { path: window.location.pathname, analyticsId }; const options: Options = { path: window.location.pathname, analyticsId };
try { try {
getFID((metric) => sendToAnalytics(metric, options)); onFID((metric) => sendToSpeedInsights(metric, options));
getTTFB((metric) => sendToAnalytics(metric, options)); onTTFB((metric) => sendToSpeedInsights(metric, options));
getLCP((metric) => sendToAnalytics(metric, options)); onLCP((metric) => sendToSpeedInsights(metric, options));
getCLS((metric) => sendToAnalytics(metric, options)); onCLS((metric) => sendToSpeedInsights(metric, options));
getFCP((metric) => sendToAnalytics(metric, options)); onFCP((metric) => sendToSpeedInsights(metric, options));
} catch (err) { } catch (err) {
console.error('[Analytics]', err); console.error('[Speed Insights]', err);
} }
} }
const mode = (import.meta as any).env.MODE as 'development' | 'production'; const mode = (import.meta as any).env.MODE as 'development' | 'production';
inject({ mode });
if (mode === 'production') { if (mode === 'production') {
webVitals(); collectWebVitals();
} }

View file

@ -6,10 +6,17 @@ import {
type DevImageService, type DevImageService,
type VercelImageConfig, type VercelImageConfig,
} from '../image/shared.js'; } from '../image/shared.js';
import { exposeEnv } from '../lib/env.js';
import { emptyDir, getVercelOutput, writeJson } from '../lib/fs.js'; import { emptyDir, getVercelOutput, writeJson } from '../lib/fs.js';
import { isServerLikeOutput } from '../lib/prerender.js'; import { isServerLikeOutput } from '../lib/prerender.js';
import { getRedirects } from '../lib/redirects.js'; import { getRedirects } from '../lib/redirects.js';
import {
getSpeedInsightsViteConfig,
type VercelSpeedInsightsConfig,
} from '../lib/speed-insights.js';
import {
getInjectableWebAnalyticsContent,
type VercelWebAnalyticsConfig,
} from '../lib/web-analytics.js';
const PACKAGE_NAME = '@astrojs/vercel/static'; const PACKAGE_NAME = '@astrojs/vercel/static';
@ -34,7 +41,12 @@ function getAdapter(): AstroAdapter {
} }
export interface VercelStaticConfig { export interface VercelStaticConfig {
/**
* @deprecated
*/
analytics?: boolean; analytics?: boolean;
webAnalytics?: VercelWebAnalyticsConfig;
speedInsights?: VercelSpeedInsightsConfig;
imageService?: boolean; imageService?: boolean;
imagesConfig?: VercelImageConfig; imagesConfig?: VercelImageConfig;
devImageService?: DevImageService; devImageService?: DevImageService;
@ -42,6 +54,8 @@ export interface VercelStaticConfig {
export default function vercelStatic({ export default function vercelStatic({
analytics, analytics,
webAnalytics,
speedInsights,
imageService, imageService,
imagesConfig, imagesConfig,
devImageService = 'sharp', devImageService = 'sharp',
@ -51,12 +65,25 @@ export default function vercelStatic({
return { return {
name: '@astrojs/vercel', name: '@astrojs/vercel',
hooks: { hooks: {
'astro:config:setup': ({ command, config, injectScript, updateConfig }) => { 'astro:config:setup': async ({ command, config, injectScript, updateConfig, logger }) => {
if (command === 'build' && analytics) { if (webAnalytics?.enabled || analytics) {
injectScript('page', 'import "@astrojs/vercel/analytics"'); if (analytics) {
logger.warn(
`The \`analytics\` property is deprecated. Please use the new \`webAnalytics\` and \`speedInsights\` properties instead.`
);
}
injectScript(
'head-inline',
await getInjectableWebAnalyticsContent({
mode: command === 'dev' ? 'development' : 'production',
})
);
}
if (command === 'build' && (speedInsights?.enabled || analytics)) {
injectScript('page', 'import "@astrojs/vercel/speed-insights"');
} }
const outDir = new URL('./static/', getVercelOutput(config.root)); const outDir = new URL('./static/', getVercelOutput(config.root));
const viteDefine = exposeEnv(['VERCEL_ANALYTICS_ID']);
updateConfig({ updateConfig({
outDir, outDir,
build: { build: {
@ -64,7 +91,7 @@ export default function vercelStatic({
redirects: false, redirects: false,
}, },
vite: { vite: {
define: viteDefine, ...getSpeedInsightsViteConfig(speedInsights?.enabled || analytics),
}, },
...getAstroImageConfig( ...getAstroImageConfig(
imageService, imageService,

View file

@ -0,0 +1,3 @@
import type { AnalyticsProps } from '@vercel/analytics';
export type VercelWebAnalyticsBeforeSend = AnalyticsProps['beforeSend'];

View file

@ -0,0 +1,10 @@
import { defineConfig } from 'astro/config';
import vercel from '@astrojs/vercel/serverless';
export default defineConfig({
adapter: vercel({
speedInsights: {
enabled: true
}
})
});

View file

@ -0,0 +1,9 @@
{
"name": "@test/astro-vercel-with-speed-insights-enabled-output-as-server",
"version": "0.0.0",
"private": true,
"dependencies": {
"@astrojs/vercel": "workspace:*",
"astro": "workspace:*"
}
}

View file

@ -0,0 +1,8 @@
<html>
<head>
<title>One</title>
</head>
<body>
<h1>One</h1>
</body>
</html>

View file

@ -0,0 +1,8 @@
<html>
<head>
<title>Two</title>
</head>
<body>
<h1>Two</h1>
</body>
</html>

View file

@ -0,0 +1,10 @@
import { defineConfig } from 'astro/config';
import vercel from '@astrojs/vercel/static';
export default defineConfig({
adapter: vercel({
speedInsights: {
enabled: true
}
})
});

View file

@ -0,0 +1,9 @@
{
"name": "@test/astro-vercel-with-speed-insights-enabled-output-as-static",
"version": "0.0.0",
"private": true,
"dependencies": {
"@astrojs/vercel": "workspace:*",
"astro": "workspace:*"
}
}

View file

@ -0,0 +1,8 @@
<html>
<head>
<title>One</title>
</head>
<body>
<h1>One</h1>
</body>
</html>

View file

@ -0,0 +1,8 @@
<html>
<head>
<title>Two</title>
</head>
<body>
<h1>Two</h1>
</body>
</html>

View file

@ -0,0 +1,10 @@
import { defineConfig } from 'astro/config';
import vercel from '@astrojs/vercel/static';
export default defineConfig({
adapter: vercel({
webAnalytics: {
enabled: true
}
})
});

View file

@ -0,0 +1,9 @@
{
"name": "@test/astro-vercel-with-web-analytics-enabled-output-as-static",
"version": "0.0.0",
"private": true,
"dependencies": {
"@astrojs/vercel": "workspace:*",
"astro": "workspace:*"
}
}

View file

@ -0,0 +1,8 @@
<html>
<head>
<title>One</title>
</head>
<body>
<h1>One</h1>
</body>
</html>

View file

@ -0,0 +1,8 @@
<html>
<head>
<title>Two</title>
</head>
<body>
<h1>Two</h1>
</body>
</html>

View file

@ -0,0 +1,46 @@
import { loadFixture } from './test-utils.js';
import { expect } from 'chai';
describe('Vercel Speed Insights', () => {
describe('output: server', () => {
/** @type {import('./test-utils.js').Fixture} */
let fixture;
before(async () => {
fixture = await loadFixture({
root: './fixtures/with-speed-insights-enabled/output-as-server/',
output: 'server',
});
await fixture.build();
});
it('ensures that Vercel Speed Insights is present in the bundle', async () => {
const [page] = await fixture.readdir('../.vercel/output/static/_astro');
const bundle = await fixture.readFile(`../.vercel/output/static/_astro/${page}`);
expect(bundle).to.contain('https://vitals.vercel-analytics.com/v1/vitals');
});
});
describe('output: static', () => {
/** @type {import('./test-utils.js').Fixture} */
let fixture;
before(async () => {
fixture = await loadFixture({
root: './fixtures/with-speed-insights-enabled/output-as-static/',
output: 'static',
});
await fixture.build();
});
it('ensures that Vercel Speed Insights is present in the bundle', async () => {
const [page] = await fixture.readdir('../.vercel/output/static/_astro');
const bundle = await fixture.readFile(`../.vercel/output/static/_astro/${page}`);
expect(bundle).to.contain('https://vitals.vercel-analytics.com/v1/vitals');
});
});
});

View file

@ -0,0 +1,25 @@
import { loadFixture } from './test-utils.js';
import { expect } from 'chai';
describe('Vercel Web Analytics', () => {
describe('output: static', () => {
/** @type {import('./test-utils.js').Fixture} */
let fixture;
before(async () => {
fixture = await loadFixture({
root: './fixtures/with-web-analytics-enabled/output-as-static/',
output: 'static',
});
await fixture.build();
});
it('ensures that Vercel Web Analytics is present in the header', async () => {
const pageOne = await fixture.readFile('../.vercel/output/static/one/index.html');
const pageTwo = await fixture.readFile('../.vercel/output/static/two/index.html');
expect(pageOne).to.contain('/_vercel/insights/script.js');
expect(pageTwo).to.contain('/_vercel/insights/script.js');
});
});
});

View file

@ -4840,6 +4840,42 @@ importers:
specifier: workspace:* specifier: workspace:*
version: link:../../../../../astro version: link:../../../../../astro
packages/integrations/vercel/test/fixtures/with-speed-insights-enabled/output-as-server:
dependencies:
'@astrojs/vercel':
specifier: workspace:*
version: link:../../../..
astro:
specifier: workspace:*
version: link:../../../../../../astro
packages/integrations/vercel/test/fixtures/with-speed-insights-enabled/output-as-static:
dependencies:
'@astrojs/vercel':
specifier: workspace:*
version: link:../../../..
astro:
specifier: workspace:*
version: link:../../../../../../astro
packages/integrations/vercel/test/fixtures/with-web-analytics-enabled/output-as-server:
dependencies:
'@astrojs/vercel':
specifier: workspace:*
version: link:../../../..
astro:
specifier: workspace:*
version: link:../../../../../../astro
packages/integrations/vercel/test/fixtures/with-web-analytics-enabled/output-as-static:
dependencies:
'@astrojs/vercel':
specifier: workspace:*
version: link:../../../..
astro:
specifier: workspace:*
version: link:../../../../../../astro
packages/integrations/vercel/test/hosted/hosted-astro-project: packages/integrations/vercel/test/hosted/hosted-astro-project:
dependencies: dependencies:
'@astrojs/vercel': '@astrojs/vercel':