Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add custom pageview property support #16

Open
wants to merge 2 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .prettierrc
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,5 @@
"trailingComma": "none",
"printWidth": 100,
"plugins": ["prettier-plugin-svelte"],
"pluginSearchDirs": ["."],
dgrebb marked this conversation as resolved.
Show resolved Hide resolved
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
}
38 changes: 34 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ npm i --save-dev @accuser/svelte-plausible-analytics

## Examples

Add Plausible Analytics to the root layout to track page views.
### Add Plausible Analytics to the root layout to track page views.

```svelte
<script>
Expand All @@ -25,7 +25,37 @@ Add Plausible Analytics to the root layout to track page views.
<slot />
```

Track analytics events:
### Add custom properties to page views:

```svelte
<script>
import { page } from "$app/stores";
import { PlausibleAnalytics } from '@accuser/svelte-plausible-analytics';

$: ({ id: route } = $page?.route); // beware duplicate plausible calls
</script>

<PlausibleAnalytics
enabled={true}
pageviewProps={{
route,
willNotBeIncluded: false,
message: `a template literal containing a ${dynamicValue}`,
testingFilter: "test-some-scenario",
"hyphenated-property": "filter value",
}}
/>

<slot />
```

Set custom properties in the `pageviewProps` Svelte prop on the `<PlausibleAnalytics />` component. *Beware hydration race conditions and take note when `<PlausibleAnalytics />` is mounted. Especially with SSG.*

The Plausible-required `event-` prefix can be omitted. Eg. `<PlausibleAnalytics pageviewProps={{"my-fancy-prop": "a value"}} />` becomes `<script src="https://plausible.io/js/script.pageview-props.js" event-my-fancy-prop"="a value"></script>`.

*Note*: as per the [Plausible documentation](https://plausible.io/docs/custom-props/introduction#limits), up to 30 custom properties can be included alongside a pageview by adding multiple attributes. There is also a 300/2000 character limit on each property `key` and `value`, respectively.

### Track analytics events:

```svelte
<script>
Expand All @@ -37,7 +67,7 @@ Track analytics events:
<button on:click={login('Button')}>Click to login!</button>
```

Track custom events:
### Track custom events:

```svelte
<script>
Expand All @@ -55,4 +85,4 @@ Matthew Gibbons - [@accuser](https://github.com/accuser)

Jeffrey Palmer - [@JeffreyPalmer](https://github.com/JeffreyPalmer)

Dan Grebb - [@dgrebb](https://github.com/dgrebb)
Dan Grebb - [@dgrebb](https://github.com/dgrebb)
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,8 @@
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"test:unit": "vitest",
"lint": "prettier --plugin-search-dir . --check . && eslint .",
dgrebb marked this conversation as resolved.
Show resolved Hide resolved
"format": "prettier --plugin-search-dir . --write ."
"lint": "prettier --check . && eslint .",
"format": "prettier --write ."
},
"exports": {
".": {
Expand Down
85 changes: 83 additions & 2 deletions src/lib/PlausibleAnalytics.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,19 @@
plausible: PlausibleTracker;
}

/**
* Type definition for properties that can be sent as part of a page view event to Plausible.
* These properties can be of type boolean, number, or string.
*/
export type PageviewProp = boolean | number | string;

/**
* Type definition for pageview properties.
* Can be either a boolean or an array containing a record with keys of type number or string,
* and values of type boolean, number, or string.
*/
export type PageviewProps = { [key: number | string]: PageviewProp } | boolean;

declare let window: PlausibleWindow;

const plausible: PlausibleTracker = (event, options) => window.plausible(event, options);
Expand Down Expand Up @@ -85,14 +98,76 @@
*/
export let outboundLinks = false;

/**
* Holds the pageview properties to be used in the application.
* The properties can be used for filtering in the Plausible dashboard. The pageview properties
* are subject to certain limitations:
* - Limited to 30 total custom properties per event.
* - Limited to 300 characters per property name.
* - Limited to 2000 characters per property value.
*
* @link https://plausible.io/docs/guided-tour#filtering for dashboard filtering by custom property details.
* @defaultValue `false` Indicates no custom properties by default.
*/
export let pageviewProps: PageviewProps = false;

/**
* Function references for property validation guards.
* These are dynamically imported during development for validation purposes.
*/
let isCustomPropsLimit: Function, isCustomPropEntryLimit: Function;

/**
* Sets the pageview properties as attributes on a given HTML script element.
* This function is responsible for validating and applying the custom properties
* defined in `pageviewProps` to the provided HTML element.
*
* @param {HTMLScriptElement} node - The HTML script element on which to set the attributes.
*/
const setPageviewProps: (node: HTMLScriptElement) => void = async (node) => {
// Early exit if pageviewProps is not defined or falsy.
if (!pageviewProps) return;

// In development mode, load validation guards dynamically and validate custom property limits.
if (dev) {
const guards = await import('./guards.js');
const length = Object.entries(pageviewProps).length;
({ isCustomPropsLimit, isCustomPropEntryLimit } = guards);

// Validate the total number of custom properties against Plausible's limit.
if (!isCustomPropsLimit(pageviewProps)) {
throw Error(
`Plausible Analytics has a limit of 30 custom properties per event. ${length} properties counted.`
);
}
}

// Iterate over each key-value pair in pageviewProps to set them as attributes.
Object.entries(pageviewProps).forEach(([key, value]) => {
// In development mode, validate the length of property names and values.
if (dev) {
if (!isCustomPropEntryLimit(300, key)) {
throw Error(`Plausible Analytics limit custom property names to 300 characters.`);
}
if (!isCustomPropEntryLimit(2000, value)) {
throw Error(`Plausible Analytics limit custom property values to 3000 characters.`);
};
}

// Set the attribute on the script node. Format: 'event-key'
node.setAttribute(`event-${key}`, String(value));
});
};

$: api = `${apiHost}/api/event`;
$: src = [
$: plausibleSrc = [
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

`${apiHost}/js/script`,
compat ? 'compat' : undefined,
fileDownloads ? 'file-downloads' : undefined,
hash ? 'hash' : undefined,
local ? 'local' : undefined,
outboundLinks ? 'outbound-links' : undefined,
pageviewProps ? 'pageview-props' : undefined,
'js'
]
.filter(Boolean)
Expand All @@ -101,7 +176,13 @@

<svelte:head>
{#if enabled}
<script data-api={api} data-domain={domain.toString()} defer {src}></script>
<script
data-api={api}
data-domain={domain.toString()}
defer
src={plausibleSrc}
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use:setPageviewProps
></script>
<script>
window.plausible =
window.plausible ||
Expand Down
92 changes: 92 additions & 0 deletions src/lib/guards.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { type PageviewProp, type PageviewProps } from './PlausibleAnalytics.svelte';

/**
* Handles various value types and issues warnings if the values are not suitable for passing to Plausible.
* Specifically checks for DOMTokenList, HTMLInputElement, Array, RegExp, and Date instances,
* and warns about potential errors if these are not parsed as strings.
* @see {@link https://plausible.io/docs/custom-props/introduction#accepted-values}
*
* @param {unknown} value - The value to be handled and checked.
* @returns {boolean} Returns true if the value is acceptable, otherwise throws an error.
* @throws {Error} Throws an error if the value is neither a number nor a string.
*/
export const handleEntry = function handleEntry(entry: unknown): boolean {
const typeName = Object.prototype.toString.call(entry).slice(8, -1);

const warn = (type: string) =>
console.warn(
`Warning: Passing ${type} to Plausible may result in error unless parsed as a string.`
);

switch (typeName) {
case 'DOMTokenList':
case 'HTMLInputElement':
case 'Array':
case 'RegExp':
case 'Date':
warn(typeName);
return true;

case 'String':
case 'Number':
return true;

default:
console.warn(
`Plausible Error: Custom property entry ${entry} is not a boolean, number, or string.`
);
return false;
}
};

/**
* Checks if the number of custom properties in a PageviewProps object exceeds a specified limit.
* @see {@link https://plausible.io/docs/custom-props/introduction#limits}
*
* @param {PageviewProps} props - The PageviewProps object to check.
* @returns {boolean} Returns true if the number of properties is within the limit, otherwise returns false and logs an error.
*/
export const isCustomPropsLimit = function isCustomPropsLimit(
dgrebb marked this conversation as resolved.
Show resolved Hide resolved
props: PageviewProps
): props is PageviewProps {
const limit = 30;
const length = Object.entries(props).length;

if (typeof props === 'object' && length > limit) {
return false;
}

return true;
};

/**
* Checks if the length of a custom property name or value exceeds a specified limit.
* @see {@link https://plausible.io/docs/custom-props/introduction#limits}
*
* @param {number} limit - The maximum allowed character length.
* @param {PageviewProp} entry - The custom property entry to check.
* @returns {boolean} Returns true if the entry's length does not exceed the limit, otherwise logs a warning.
*/
export const isCustomPropEntryLimit = function isCustomPropEntryLimit(
limit: number,
entry: PageviewProp
): entry is PageviewProp {
const asString = entry.toString();

// No limit checks needed for boolean
if (typeof entry === 'boolean') return true;

// Check character length of number with `toString()`
if (typeof entry === 'number' && asString.length > limit) {
return false;
}

// Error if DOM node passed as prop name or value
if (!handleEntry(entry)) return false;

if (asString.length > limit) {
return false;
}

return true;
};