mirror of
https://github.com/facebook/docusaurus.git
synced 2025-04-29 02:08:36 +02:00
feat(gtag-plugin): gtag should support multiple tracking ids, notably for the UA => GA4 transition (#8620)
This commit is contained in:
parent
5b05c0e73e
commit
32384b761c
6 changed files with 241 additions and 40 deletions
|
@ -0,0 +1,156 @@
|
|||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {normalizePluginOptions} from '@docusaurus/utils-validation';
|
||||
import {
|
||||
validateOptions,
|
||||
type PluginOptions,
|
||||
type Options,
|
||||
DEFAULT_OPTIONS,
|
||||
} from '../options';
|
||||
import type {Validate} from '@docusaurus/types';
|
||||
|
||||
function testValidateOptions(options: Options) {
|
||||
return validateOptions({
|
||||
validate: normalizePluginOptions as Validate<Options, PluginOptions>,
|
||||
options,
|
||||
});
|
||||
}
|
||||
|
||||
function validationResult(options: Options) {
|
||||
return {
|
||||
id: 'default',
|
||||
...DEFAULT_OPTIONS,
|
||||
...options,
|
||||
trackingID:
|
||||
typeof options.trackingID === 'string'
|
||||
? [options.trackingID]
|
||||
: options.trackingID,
|
||||
};
|
||||
}
|
||||
|
||||
const MinimalConfig: Options = {
|
||||
trackingID: 'G-XYZ12345',
|
||||
};
|
||||
|
||||
describe('validateOptions', () => {
|
||||
it('throws for undefined options', () => {
|
||||
expect(
|
||||
// @ts-expect-error: TS should error
|
||||
() => testValidateOptions(undefined),
|
||||
).toThrowErrorMatchingInlineSnapshot(`""trackingID" is required"`);
|
||||
});
|
||||
|
||||
it('throws for null options', () => {
|
||||
expect(
|
||||
// @ts-expect-error: TS should error
|
||||
() => testValidateOptions(null),
|
||||
).toThrowErrorMatchingInlineSnapshot(`""value" must be of type object"`);
|
||||
});
|
||||
|
||||
it('throws for empty object options', () => {
|
||||
expect(
|
||||
// @ts-expect-error: TS should error
|
||||
() => testValidateOptions({}),
|
||||
).toThrowErrorMatchingInlineSnapshot(`""trackingID" is required"`);
|
||||
});
|
||||
|
||||
it('throws for number options', () => {
|
||||
expect(
|
||||
// @ts-expect-error: TS should error
|
||||
() => testValidateOptions(42),
|
||||
).toThrowErrorMatchingInlineSnapshot(`""value" must be of type object"`);
|
||||
});
|
||||
|
||||
it('throws for null trackingID', () => {
|
||||
expect(
|
||||
// @ts-expect-error: TS should error
|
||||
() => testValidateOptions({trackingID: null}),
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`""trackingID" does not match any of the allowed types"`,
|
||||
);
|
||||
});
|
||||
it('throws for number trackingID', () => {
|
||||
expect(
|
||||
// @ts-expect-error: TS should error
|
||||
() => testValidateOptions({trackingID: 42}),
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`""trackingID" does not match any of the allowed types"`,
|
||||
);
|
||||
});
|
||||
it('throws for empty trackingID', () => {
|
||||
expect(() =>
|
||||
testValidateOptions({trackingID: ''}),
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`""trackingID" does not match any of the allowed types"`,
|
||||
);
|
||||
});
|
||||
|
||||
it('accepts minimal config', () => {
|
||||
expect(testValidateOptions(MinimalConfig)).toEqual(
|
||||
validationResult(MinimalConfig),
|
||||
);
|
||||
});
|
||||
|
||||
it('accepts anonymizeIP', () => {
|
||||
const config: Options = {
|
||||
...MinimalConfig,
|
||||
anonymizeIP: true,
|
||||
};
|
||||
expect(testValidateOptions(config)).toEqual(validationResult(config));
|
||||
});
|
||||
|
||||
it('accepts single trackingID', () => {
|
||||
const config: Options = {
|
||||
trackingID: 'G-ABCDEF123',
|
||||
};
|
||||
expect(testValidateOptions(config)).toEqual(validationResult(config));
|
||||
});
|
||||
|
||||
it('accepts multiple trackingIDs', () => {
|
||||
const config: Options = {
|
||||
trackingID: ['G-ABCDEF123', 'UA-XYZ456789'],
|
||||
};
|
||||
expect(testValidateOptions(config)).toEqual(validationResult(config));
|
||||
});
|
||||
|
||||
it('throws for empty trackingID arrays', () => {
|
||||
const config: Options = {
|
||||
// @ts-expect-error: TS should error
|
||||
trackingID: [],
|
||||
};
|
||||
expect(() =>
|
||||
testValidateOptions(config),
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`""trackingID" does not match any of the allowed types"`,
|
||||
);
|
||||
});
|
||||
|
||||
it('throws for sparse trackingID arrays', () => {
|
||||
const config: Options = {
|
||||
// @ts-expect-error: TS should error
|
||||
trackingID: ['G-ABCDEF123', null, 'UA-XYZ456789'],
|
||||
};
|
||||
expect(() =>
|
||||
testValidateOptions(config),
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`""trackingID" does not match any of the allowed types"`,
|
||||
);
|
||||
});
|
||||
|
||||
it('throws for bad trackingID arrays', () => {
|
||||
const config: Options = {
|
||||
// @ts-expect-error: TS should error
|
||||
trackingID: ['G-ABCDEF123', 42],
|
||||
};
|
||||
expect(() =>
|
||||
testValidateOptions(config),
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`""trackingID" does not match any of the allowed types"`,
|
||||
);
|
||||
});
|
||||
});
|
|
@ -5,23 +5,38 @@
|
|||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {Joi} from '@docusaurus/utils-validation';
|
||||
import type {
|
||||
LoadContext,
|
||||
Plugin,
|
||||
OptionValidationContext,
|
||||
ThemeConfig,
|
||||
ThemeConfigValidationContext,
|
||||
} from '@docusaurus/types';
|
||||
import type {LoadContext, Plugin} from '@docusaurus/types';
|
||||
import type {PluginOptions, Options} from './options';
|
||||
|
||||
function createConfigSnippet({
|
||||
trackingID,
|
||||
anonymizeIP,
|
||||
}: {
|
||||
trackingID: string;
|
||||
anonymizeIP: boolean;
|
||||
}): string {
|
||||
return `gtag('config', '${trackingID}', { ${
|
||||
anonymizeIP ? "'anonymize_ip': true" : ''
|
||||
} });`;
|
||||
}
|
||||
|
||||
function createConfigSnippets({
|
||||
trackingID: trackingIDArray,
|
||||
anonymizeIP,
|
||||
}: PluginOptions): string {
|
||||
return trackingIDArray
|
||||
.map((trackingID) => createConfigSnippet({trackingID, anonymizeIP}))
|
||||
.join('\n');
|
||||
}
|
||||
|
||||
export default function pluginGoogleGtag(
|
||||
context: LoadContext,
|
||||
options: PluginOptions,
|
||||
): Plugin {
|
||||
const {anonymizeIP, trackingID} = options;
|
||||
const isProd = process.env.NODE_ENV === 'production';
|
||||
|
||||
const firstTrackingId = options.trackingID[0];
|
||||
|
||||
return {
|
||||
name: 'docusaurus-plugin-google-gtag',
|
||||
|
||||
|
@ -60,7 +75,11 @@ export default function pluginGoogleGtag(
|
|||
tagName: 'script',
|
||||
attributes: {
|
||||
async: true,
|
||||
src: `https://www.googletagmanager.com/gtag/js?id=${trackingID}`,
|
||||
// We only include the first tracking id here because google says
|
||||
// we shouldn't install multiple tags/scripts on the same page
|
||||
// Instead we should load one script and use n * gtag("config",id)
|
||||
// See https://developers.google.com/tag-platform/gtagjs/install#add-products
|
||||
src: `https://www.googletagmanager.com/gtag/js?id=${firstTrackingId}`,
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -69,9 +88,8 @@ export default function pluginGoogleGtag(
|
|||
window.dataLayer = window.dataLayer || [];
|
||||
function gtag(){dataLayer.push(arguments);}
|
||||
gtag('js', new Date());
|
||||
gtag('config', '${trackingID}', { ${
|
||||
anonymizeIP ? "'anonymize_ip': true" : ''
|
||||
} });`,
|
||||
${createConfigSnippets(options)};
|
||||
`,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
@ -79,27 +97,6 @@ export default function pluginGoogleGtag(
|
|||
};
|
||||
}
|
||||
|
||||
const pluginOptionsSchema = Joi.object<PluginOptions>({
|
||||
trackingID: Joi.string().required(),
|
||||
anonymizeIP: Joi.boolean().default(false),
|
||||
});
|
||||
|
||||
export function validateOptions({
|
||||
validate,
|
||||
options,
|
||||
}: OptionValidationContext<Options, PluginOptions>): PluginOptions {
|
||||
return validate(pluginOptionsSchema, options);
|
||||
}
|
||||
|
||||
export function validateThemeConfig({
|
||||
themeConfig,
|
||||
}: ThemeConfigValidationContext<ThemeConfig>): ThemeConfig {
|
||||
if ('gtag' in themeConfig) {
|
||||
throw new Error(
|
||||
'The "gtag" field in themeConfig should now be specified as option for plugin-google-gtag. More information at https://github.com/facebook/docusaurus/pull/5832.',
|
||||
);
|
||||
}
|
||||
return themeConfig;
|
||||
}
|
||||
export {validateThemeConfig, validateOptions} from './options';
|
||||
|
||||
export type {PluginOptions, Options};
|
||||
|
|
|
@ -4,10 +4,58 @@
|
|||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
import {Joi} from '@docusaurus/utils-validation';
|
||||
import type {
|
||||
OptionValidationContext,
|
||||
ThemeConfig,
|
||||
ThemeConfigValidationContext,
|
||||
} from '@docusaurus/types';
|
||||
|
||||
export type PluginOptions = {
|
||||
trackingID: string;
|
||||
trackingID: [string, ...string[]];
|
||||
// TODO deprecate anonymizeIP after June 2023
|
||||
// "In Google Analytics 4, IP masking is not necessary
|
||||
// since IP addresses are not logged or stored."
|
||||
// https://support.google.com/analytics/answer/2763052?hl=en
|
||||
anonymizeIP: boolean;
|
||||
};
|
||||
|
||||
export type Options = Partial<PluginOptions>;
|
||||
export type Options = {
|
||||
trackingID: string | [string, ...string[]];
|
||||
anonymizeIP?: boolean;
|
||||
};
|
||||
|
||||
export const DEFAULT_OPTIONS: Partial<PluginOptions> = {
|
||||
anonymizeIP: false,
|
||||
};
|
||||
|
||||
const pluginOptionsSchema = Joi.object<PluginOptions>({
|
||||
// We normalize trackingID as a string[]
|
||||
trackingID: Joi.alternatives()
|
||||
.try(
|
||||
Joi.alternatives().conditional(Joi.string().required(), {
|
||||
then: Joi.custom((val: boolean) => [val]),
|
||||
}),
|
||||
Joi.array().items(Joi.string().required()),
|
||||
)
|
||||
.required(),
|
||||
anonymizeIP: Joi.boolean().default(DEFAULT_OPTIONS.anonymizeIP),
|
||||
});
|
||||
|
||||
export function validateOptions({
|
||||
validate,
|
||||
options,
|
||||
}: OptionValidationContext<Options, PluginOptions>): PluginOptions {
|
||||
return validate(pluginOptionsSchema, options);
|
||||
}
|
||||
|
||||
export function validateThemeConfig({
|
||||
themeConfig,
|
||||
}: ThemeConfigValidationContext<ThemeConfig>): ThemeConfig {
|
||||
if ('gtag' in themeConfig) {
|
||||
throw new Error(
|
||||
'The "gtag" field in themeConfig should now be specified as option for plugin-google-gtag. More information at https://github.com/facebook/docusaurus/pull/5832.',
|
||||
);
|
||||
}
|
||||
return themeConfig;
|
||||
}
|
||||
|
|
|
@ -10,6 +10,6 @@
|
|||
"rootDir": "src",
|
||||
"outDir": "lib"
|
||||
},
|
||||
"include": ["src/gtag.ts", "src/options.ts", "src/*.d.ts"],
|
||||
"include": ["src/gtag.ts", "src/*.d.ts"],
|
||||
"exclude": ["**/__tests__/**"]
|
||||
}
|
||||
|
|
|
@ -45,7 +45,7 @@ Accepted fields:
|
|||
|
||||
| Name | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `trackingID` | `string` | **Required** | The tracking ID of your gtag service. |
|
||||
| `trackingID` | <code>string \| string[]</code> | **Required** | The tracking ID of your gtag service. It is possible to provide multiple ids. |
|
||||
| `anonymizeIP` | `boolean` | `false` | Whether the IP should be anonymized when sending requests. |
|
||||
|
||||
```mdx-code-block
|
||||
|
|
|
@ -357,7 +357,7 @@ const config = {
|
|||
},
|
||||
gtag: !(isDeployPreview || isBranchDeploy)
|
||||
? {
|
||||
trackingID: 'UA-141789564-1',
|
||||
trackingID: ['G-E5CR2Q1NRE', 'UA-141789564-1'],
|
||||
}
|
||||
: undefined,
|
||||
sitemap: {
|
||||
|
|
Loading…
Add table
Reference in a new issue