From 8384b1ae1076716f1a1f93312003b7b805ec0834 Mon Sep 17 00:00:00 2001 From: Alejandro Haydar Date: Wed, 16 Apr 2025 07:38:18 -0400 Subject: [PATCH 01/18] Shadcn UI Theme Variables Implementation from Design System Overview This PR implements the design system's color palette and theme variables into the Shadcn UI configuration, ensuring consistent styling across the documentation site. Key Changes 1. Theme Variables Implementation src/css/custom.css: Added design system color variables for both light and dark themes Implemented proper HSL color values for all Shadcn UI components Added smooth transitions between theme states Configured proper color contrast ratios 2. Color Palette Integration Apply to custom.css } 3. Component Updates src/components/ui/button.jsx: Updated to use new theme variables Ensured proper color application across variants Added smooth transitions for state changes src/components/ui/theme-switcher.jsx: Updated to handle new theme variables Improved theme transition handling Added proper icon color transitions Testing Instructions Theme Variables: Verify all color variables match design system Check color contrast ratios Test transitions between themes Component Styling: Verify components use correct theme variables Check hover and active states Test in both light and dark modes Accessibility: Verify proper color contrast Test focus states Check text readability Review Checklist [ ] Verify all color variables match design system [ ] Test color transitions between themes [ ] Check component styling in both themes [ ] Verify accessibility standards [ ] Test responsive behavior [ ] Ensure consistent color application --- src/components/MuiTheme/index.js | 6 +- src/components/ui/button.jsx | 12 +-- src/components/ui/navigation.jsx | 50 +++++++++++ src/components/ui/theme-switcher.jsx | 33 +++++++ src/css/custom.css | 128 +++++++++++++++++---------- src/pages/tailwind-test.js | 2 + 6 files changed, 175 insertions(+), 56 deletions(-) create mode 100644 src/components/ui/navigation.jsx create mode 100644 src/components/ui/theme-switcher.jsx diff --git a/src/components/MuiTheme/index.js b/src/components/MuiTheme/index.js index 9257a65aaa..f7084db71b 100644 --- a/src/components/MuiTheme/index.js +++ b/src/components/MuiTheme/index.js @@ -9,17 +9,17 @@ const extTheme = extendTheme({ main: '#6638b8', }, background: { - default: '#f2f4f7', + default: '#F9FAFB', }, }, }, dark: { palette: { primary: { - main: '#6638b8', + main: '#916CE7', }, background: { - default: '#f2f4f7', + default: '#050505', }, }, }, diff --git a/src/components/ui/button.jsx b/src/components/ui/button.jsx index 2b18329f30..a573d1474d 100644 --- a/src/components/ui/button.jsx +++ b/src/components/ui/button.jsx @@ -4,20 +4,20 @@ import { cva } from "class-variance-authority"; import { cn } from "../../lib/utils.js"; const buttonVariants = cva( - "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50", + "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 border-none", { variants: { variant: { default: "bg-primary text-primary-foreground shadow hover:bg-primary/90", destructive: - "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90", + "bg-secondary text-destructive border-solid border border-destructive shadow-sm hover:bg-destructive/10", outline: - "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground", + "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80 border-solid border border-border hover:bg-accent hover:text-accent-foreground", secondary: - "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80", - ghost: "hover:bg-accent hover:text-accent-foreground", - link: "text-primary underline-offset-4 hover:underline", + "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80 border-solid border border-border hover:bg-accent hover:text-accent-foreground", + ghost: "bg-transparent hover:bg-accent hover:text-accent-foreground", + link: "bg-transparent text-primary underline-offset-4 hover:underline", }, size: { default: "h-9 px-4 py-2", diff --git a/src/components/ui/navigation.jsx b/src/components/ui/navigation.jsx new file mode 100644 index 0000000000..bef1463ae2 --- /dev/null +++ b/src/components/ui/navigation.jsx @@ -0,0 +1,50 @@ +import React from "react"; +import { cn } from "../../lib/utils.js"; +import { + Home, + BookOpen, + Settings, + User, + ChevronDown, + Menu +} from "lucide-react"; +import ThemeSwitcher from "./theme-switcher.jsx"; + +const Navigation = ({ className }) => { + return ( + + ); +}; + +export default Navigation; \ No newline at end of file diff --git a/src/components/ui/theme-switcher.jsx b/src/components/ui/theme-switcher.jsx new file mode 100644 index 0000000000..2779264d28 --- /dev/null +++ b/src/components/ui/theme-switcher.jsx @@ -0,0 +1,33 @@ +import React from 'react'; +import { useColorMode } from '@docusaurus/theme-common'; +import { Sun, Moon } from 'lucide-react'; +import { Button } from './button.jsx'; + +const ThemeSwitcher = () => { + const { colorMode, setColorMode } = useColorMode(); + + const toggleTheme = () => { + const newTheme = colorMode === 'dark' ? 'light' : 'dark'; + setColorMode(newTheme); + // Ensure the theme is applied to the root element + document.documentElement.setAttribute('data-theme', newTheme); + }; + + return ( + + ); +}; + +export default ThemeSwitcher; \ No newline at end of file diff --git a/src/css/custom.css b/src/css/custom.css index faa094226a..0be2f6c586 100644 --- a/src/css/custom.css +++ b/src/css/custom.css @@ -2,52 +2,7 @@ @tailwind components; @tailwind utilities; -@layer base { - :root { - --background: 0 0% 100%; - --foreground: 222.2 84% 4.9%; - --card: 0 0% 100%; - --card-foreground: 222.2 84% 4.9%; - --popover: 0 0% 100%; - --popover-foreground: 222.2 84% 4.9%; - --primary: 222.2 47.4% 11.2%; - --primary-foreground: 210 40% 98%; - --secondary: 210 40% 96.1%; - --secondary-foreground: 222.2 47.4% 11.2%; - --muted: 210 40% 96.1%; - --muted-foreground: 215.4 16.3% 46.9%; - --accent: 210 40% 96.1%; - --accent-foreground: 222.2 47.4% 11.2%; - --destructive: 0 84.2% 60.2%; - --destructive-foreground: 210 40% 98%; - --border: 214.3 31.8% 91.4%; - --input: 214.3 31.8% 91.4%; - --ring: 222.2 84% 4.9%; - --radius: 0.5rem; - } - .dark { - --background: 222.2 84% 4.9%; - --foreground: 210 40% 98%; - --card: 222.2 84% 4.9%; - --card-foreground: 210 40% 98%; - --popover: 222.2 84% 4.9%; - --popover-foreground: 210 40% 98%; - --primary: 210 40% 98%; - --primary-foreground: 222.2 47.4% 11.2%; - --secondary: 217.2 32.6% 17.5%; - --secondary-foreground: 210 40% 98%; - --muted: 217.2 32.6% 17.5%; - --muted-foreground: 215 20.2% 65.1%; - --accent: 217.2 32.6% 17.5%; - --accent-foreground: 210 40% 98%; - --destructive: 0 62.8% 30.6%; - --destructive-foreground: 210 40% 98%; - --border: 217.2 32.6% 17.5%; - --input: 217.2 32.6% 17.5%; - --ring: 212.7 26.8% 83.9%; - } -} /** * Any CSS included here will be global. The classic template @@ -97,7 +52,7 @@ /* You can override the default Infima variables here. */ :root { - --ifm-background-color: #f2f4f7; + --ifm-background-color: var(--background); --ifm-color-primary: #6638b8; --ifm-color-primary-dark: #522d93; --ifm-color-primary-darker: #472781; @@ -112,7 +67,7 @@ --ifm-code-padding-vertical: 0.05rem; --ifm-font-family-base: 'Roboto', Arial, Helvetica, sans-serif; --ifm-menu-color-active: #6638b8; - --ifm-font-color-base: #000000; + --ifm-font-color-base: var(--foreground); --ifm-font-color-base-inverse: rgb(218, 221, 225); --ifm-toc-padding-horizontal: 20px; --docusaurus-highlighted-code-line-bg: rgba(224, 199, 50, 0.2); @@ -121,8 +76,11 @@ --ifm-menu-link-sublist-icon: url('data:image/svg+xml;utf8,'); } + /* For readability concerns, you should choose a lighter palette in dark mode. */ [data-theme='dark'] { + --ifm-background-color: var(--background ); + --ifm-menu-color-active: #b198df; --ifm-color-primary: #b198df; --ifm-color-primary-dark: #522d93; @@ -138,6 +96,82 @@ --ifm-font-color-base-inverse: #000000; } +/** + * Shadcn UI theme + */ + @layer base { + :root { + --background: 210 20% 98.04%; + --foreground: 220.91 39.29% 10.98%; + --muted: 220 13.04% 90.98%; + --muted-foreground: 220 8.94% 46.08%; + --popover: 0 0% 100%; + --popover-foreground: 220.91 39.29% 10.98%; + --card: 0 0% 100%; + --card-foreground: 220.91 39.29% 10.98%; + --border: 220 13.04% 90.98%; + --input: 220 13.04% 90.98%; + --primary: 261.56 53.33% 47.06%; + --primary-foreground: 0 0% 100%; + --secondary: 0 0% 100%; + --secondary-foreground: 220.91 39.29% 10.98%; + --accent: 220 14.29% 95.88%; + --accent-foreground: 220.91 39.29% 10.98%; + --destructive: 3.96 72.8% 50.98%; + --destructive-foreground: 0 0% 100%; + --ring: 262.11 53.27% 79.02%; + --chart-1: 262.11 53.27% 79.02%; + --chart-2: 183.6 84.27% 34.9%; + --chart-3: 40.11 80% 54.9%; + --chart-4: 339.77 74.78% 54.9%; + --chart-5: 220.45 70.08% 50.2%; + --radius: 0.5rem; + } + + [data-theme='dark'] { + --background: 0 0% 1.96%; + --foreground: 0 0% 90.2%; + --muted: 156 9% 8%; + --muted-foreground: 0 0% 60%; + --popover: 0 0% 6.27%; + --popover-foreground: 0 0% 90.2%; + --card: 0 0% 9.8%; + --card-foreground: 0 0% 90.2%; + --border: 0 0% 13.73%; + --input: 0 0% 17.65%; + --primary: 258.05 71.93% 66.47%; + --primary-foreground: 0 0% 90.2%; + --secondary: 0 0% 0%; + --secondary-foreground: 0 0% 90.2%; + --accent: 0 0% 15.69%; + --accent-foreground: 0 0% 60%; + --destructive: 3.95 72.93% 44.9%; + --destructive-foreground: 0 0% 100%; + --ring: 262.11 53.27% 79.02%; + --chart-1: 261.82 54.1% 88.04%; + --chart-2: 183.75 45.28% 58.43%; + --chart-3: 40.27 76.84% 62.75%; + --chart-4: 340 73.06% 62.16%; + --chart-5: 220.66 71.03% 58.04%; + } +} + +/* Add transition for smooth theme switching */ +* { + transition: background-color 0.3s ease, border-color 0.3s ease, color 0.3s ease; +} + +/* Ensure the root element has the correct background color */ +:root { + background-color: hsl(var(--background)); + color: hsl(var(--foreground)); +} + +/* Add transition for specific elements */ +button, a, input, select, textarea { + transition: background-color 0.3s ease, border-color 0.3s ease, color 0.3s ease; +} + [data-theme='dark'] .breadcrumbs__item--active .breadcrumbs__link { color: var(--ifm-font-color-base); background: none; diff --git a/src/pages/tailwind-test.js b/src/pages/tailwind-test.js index d36ea45584..0de479c3e6 100644 --- a/src/pages/tailwind-test.js +++ b/src/pages/tailwind-test.js @@ -14,10 +14,12 @@ import { Info } from 'lucide-react'; import { Button } from '../components/ui/button.jsx'; +import Navigation from '../components/ui/navigation.jsx'; export default function TailwindTest() { return ( +

From 10cd92685219d86841bc42ccbf04d5cdd6a9e36a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 16 Apr 2025 12:15:43 +0100 Subject: [PATCH 02/18] Release Attribution version 0.5.0 (#1215) --- .../dbt-attribution-data-model/index.md | 2 +- .../migration-guides/attribution/index.md | 13 + src/componentVersions.js | 2 +- .../Schemas/dbtAttribution_0.5.0.json | 388 ++++++++++++++++++ 4 files changed, 403 insertions(+), 2 deletions(-) create mode 100644 docs/modeling-your-data/modeling-your-data-with-dbt/migration-guides/attribution/index.md create mode 100644 src/components/JsonSchemaValidator/Schemas/dbtAttribution_0.5.0.json diff --git a/docs/modeling-your-data/modeling-your-data-with-dbt/dbt-models/dbt-attribution-data-model/index.md b/docs/modeling-your-data/modeling-your-data-with-dbt/dbt-models/dbt-attribution-data-model/index.md index 2be05418c6..d1f1ce689e 100644 --- a/docs/modeling-your-data/modeling-your-data-with-dbt/dbt-models/dbt-attribution-data-model/index.md +++ b/docs/modeling-your-data/modeling-your-data-with-dbt/dbt-models/dbt-attribution-data-model/index.md @@ -213,7 +213,7 @@ You can do either for campaigns, too, with the `snowplow__channels_to_exclude` a In order to reduce unneccesarily long paths you can apply a number of path transformations that are created as part of user defined functions automatically in your warehouse by the package. -In order to apply these transformations, all you have to do is to define them in the `snowplow__path_transforms` variable as a list of dictionaries, with the transformation name as key and optionally the parameter as value (for `remove_if_last_and_not_all` and `remove_if_not_all`). If the transformation requires no parameter you can just use `null` as values for the dictionary. For more details on how to do this, check out the [configuration page](/docs/modeling-your-data/modeling-your-data-with-dbt/dbt-configuration/attribution/index.mdx) E.g.: `{'exposure_path': null, 'remove_if_last_and_not_all': 'direct'}` +In order to apply these transformations, all you have to do is to define them in the `snowplow__path_transforms` variable as a dictionary. In case of `remove_if_last_and_not_all` and `remove_if_not_all` transformations, the transformation name is the key and a non-empty array is the value. For other transformations (`exposure_path`, `first_path`, `unique_path`), no additional parameter is required, you can just use `null` as values. For more details on how to do this, check out the [configuration page](/docs/modeling-your-data/modeling-your-data-with-dbt/dbt-configuration/attribution/index.mdx) E.g.: `{'exposure_path': null, 'remove_if_last_and_not_all': ['channel_to_remove_1', 'campaign_to_remove_1', 'campaign_to_remove_2']}` Please note that the transformations are applied on both campaign and channel paths equally.
Path transform options diff --git a/docs/modeling-your-data/modeling-your-data-with-dbt/migration-guides/attribution/index.md b/docs/modeling-your-data/modeling-your-data-with-dbt/migration-guides/attribution/index.md new file mode 100644 index 0000000000..57bfa8f755 --- /dev/null +++ b/docs/modeling-your-data/modeling-your-data-with-dbt/migration-guides/attribution/index.md @@ -0,0 +1,13 @@ +--- +title: "Attribution" +sidebar_position: 20 +--- + +### Upgrading to 0.5.0 +- From now on the `snowplow__path_transforms` variable parameters only accept non-empty arrays for `remove_if_last_and_not_all` and `remove_if_not_all` variables instead of strings, please your variable overwrites in your dbt_project.yml accordingly. Previously you could only remove one specific channel or campaign, now you can do multiple, if needed. + +```yml title="dbt_project.yml" +vars: + snowplow_attribution: + snowplow__path_transforms: {'exposure_path': null, 'remove_if_last_and_not_all': ['channel_to_remove_1', 'campaign_to_remove_1, 'campaign_to_remove_2']} + ``` diff --git a/src/componentVersions.js b/src/componentVersions.js index dac1bf7896..35d363f426 100644 --- a/src/componentVersions.js +++ b/src/componentVersions.js @@ -41,7 +41,7 @@ export const versions = { // Data Modelling // dbt - dbtSnowplowAttribution: '0.4.0', + dbtSnowplowAttribution: '0.5.0', dbtSnowplowUnified: '0.5.2', dbtSnowplowWeb: '1.0.1', dbtSnowplowMobile: '1.0.0', diff --git a/src/components/JsonSchemaValidator/Schemas/dbtAttribution_0.5.0.json b/src/components/JsonSchemaValidator/Schemas/dbtAttribution_0.5.0.json new file mode 100644 index 0000000000..838d0417f6 --- /dev/null +++ b/src/components/JsonSchemaValidator/Schemas/dbtAttribution_0.5.0.json @@ -0,0 +1,388 @@ +{ + "definitions": { + "passthrough_vars": { + "type": "array", + "description": "> Click the plus sign to add a new entry", + "minItems": 0, + "items": { + "title": "Type", + "oneOf": [ + { + "type": "string", + "title": "Column Name" + }, + { + "type": "object", + "title": "SQL & Alias", + "properties": { + "sql": { + "type": "string" + }, + "alias": { + "type": "string" + } + }, + "required": [ + "sql", + "alias" + ], + "additionalProperties": false + } + ] + }, + "uniqueItems": true + } + }, + "type": "object", + "properties": { + "snowplow__conversions_source": { + "recommendFullRefresh": true, + "order": 81, + "consoleGroup": "advanced", + "type": "string", + "title": "Conversions Source", + "description": "Source of conversion events", + "longDescription": "The source (schema and table) of your conversion events, likely the conversions table generated by the Unified package. Optionally it may be hardcoded with a string reference instead of a source with a schema.table or database.schema.table format.", + "packageDefault": "{{ source('derived', 'snowplow_unified_conversions') }}", + "group": "Warehouse and Tracker" + }, + "snowplow__conversion_path_source": { + "recommendFullRefresh": true, + "order": 76, + "consoleGroup": "advanced", + "type": "string", + "title": "Conversion Path Source", + "description": "Source of paths (touchpoints) table", + "longDescription": "The source (schema and table) of the paths (touchpoints). By default it is the derived `snowplow_unified_views` table. Optionally it may be hardcoded with a string reference instead of a source with a schema.table or database.schema.table format.", + "packageDefault": "{{ source('derived', 'snowplow_unified_views') }}", + "group": "Warehouse and Tracker" + }, + "snowplow__user_mapping_source": { + "recommendFullRefresh": true, + "order": 125, + "consoleGroup": "advanced", + "type": "string", + "title": "User Mapping Source", + "description": "Source of user mapping table to be used for stitching by default", + "longDescription": "The source (schema and table) of the user mapping table produced by the unified package. In case the user mapping table is not available, the paths_to_conversion macro needs to be overwritten in the dbt project where the package is referenced.", + "packageDefault": "{{ source('derived', 'snowplow_unified_user_mapping') }}", + "group": "Warehouse and Tracker" + }, + "snowplow__conversion_window_start_date": { + "recommendFullRefresh": true, + "order": 80, + "consoleGroup": "advanced", + "type": "string", + "format": "date", + "title": "Conversion Window Start Date", + "group": "Operation and Logic", + "longDescription": "The start date in UTC for the window of conversions to include. It is only used for the drop and recompute report tables/views.", + "packageDefault": "current_date()-31" + }, + "snowplow__conversion_window_end_date": { + "recommendFullRefresh": true, + "order": 79, + "consoleGroup": "advanced", + "type": "string", + "format": "date", + "title": "Conversion Window End Date", + "group": "Operation and Logic", + "longDescription": "The end date in UTC for the window of conversions to include. It is only used for the drop and recompute report tables/views.", + "packageDefault": "" + }, + "snowplow__conversion_window_days": { + "recommendFullRefresh": true, + "order": 78, + "consoleGroup": "advanced", + "type": "number", + "minimum": 0, + "title": "Conversion Window Days", + "group": "Operation and Logic", + "longDescription": "The last complete nth number of days (calculated from the last processed pageview within page_views_source) to dynamically update the conversion_window_start_date and end_date with. Will only apply if both variables are left as an empty string.", + "packageDefault": "30" + }, + "snowplow__path_lookback_days": { + "recommendFullRefresh": true, + "order": 18, + "consoleGroup": "basic", + "type": "number", + "minimum": 0, + "title": "Path Lookback Days", + "group": "Operation and Logic", + "longDescription": "Restricts the model to marketing channels within this many days of the conversion (values of 30, 14 or 7 are recommended).", + "packageDefault": "30" + }, + "snowplow__path_lookback_steps": { + "recommendFullRefresh": true, + "order": 19, + "consoleGroup": "basic", + "type": "number", + "minimum": 0, + "title": "Path Lookback Steps", + "group": "Operation and Logic", + "longDescription": "The limit for the number of marketing channels to look at before the conversion.", + "packageDefault": "0 (unlimited)" + }, + "snowplow__path_transforms": { + "recommendFullRefresh": false, + "order": 116, + "consoleGroup": "advanced", + "type": "array", + "description": "> Click the plus sign to add a new entry", + "minItems": 0, + "title": "Path Transforms", + "longDescription": "Dictionary of path transforms (and their argument) to perform on the conversion path (see the transform path options in our [model docs](/docs/modeling-your-data/modeling-your-data-with-dbt/dbt-models/dbt-attribution-data-model/)). For `remove_if_not_all` and `remove_if_last_and_not_all` transformations, the argument has to be a non-empty array, for the others it should be null.", + "packageDefault": "{'exposure_path': null}", + "group": "Contexts, Filters, and Logs", + "items": { + "type": "object", + "title": "Path transform", + "oneOf": [ + { + "title": "exposure", + "required": [ + "exposure" + ], + "properties": { + "exposure": { + "type": "string", + "default": "null" + } + } + }, + { + "title": "first", + "required": [ + "first" + ], + "properties": { + "first": { + "type": "string", + "default": "null" + } + } + }, + { + "title": "unique", + "required": [ + "unique" + ], + "properties": { + "unique": { + "type": "string", + "default": "null" + } + } + }, + { + "title": "remove if last and not all", + "required": [ + "remove_if_last_and_not_all" + ], + "properties": { + "remove_if_last_and_not_all": { + "type": "string" + } + } + }, + { + "title": "remove if not all", + "required": [ + "remove_if_not_all" + ], + "properties": { + "remove_if_not_all": { + "type": "string" + } + } + } + ] + }, + "uniqueItems": true + }, + "snowplow__channels_to_exclude": { + "recommendFullRefresh": true, + "order": 68, + "consoleGroup": "advanced", + "type": "array", + "description": "> Click the plus sign to add a new entry", + "minItems": 0, + "title": "Channels to Exclude", + "longDescription": "List of channels to exclude from analysis (empty to keep all channels). For example, users may want to exclude the `Direct` channel from the analysis.", + "packageDefault": "[ ] (no filter applied)", + "group": "Contexts, Filters, and Logs", + "items": { + "type": "string" + } + }, + "snowplow__channels_to_include": { + "recommendFullRefresh": true, + "order": 69, + "consoleGroup": "advanced", + "type": "array", + "description": "> Click the plus sign to add a new entry", + "minItems": 0, + "title": "Channels to Include", + "longDescription": "List of channels to include in the analysis (empty to keep all channels). For example, users may want to include the `Direct` channel only in the analysis.", + "packageDefault": "[ ] (no filter applied)", + "group": "Contexts, Filters, and Logs", + "items": { + "type": "string" + } + }, + "snowplow__campaigns_to_exclude": { + "recommendFullRefresh": true, + "order": 64, + "consoleGroup": "advanced", + "type": "array", + "description": "> Click the plus sign to add a new entry", + "minItems": 0, + "title": "Campaigns to Exclude", + "longDescription": "List of campaigns to exclude from analysis (empty to keep all campaigns).", + "packageDefault": "[ ] (no filter applied)", + "group": "Contexts, Filters, and Logs", + "items": { + "type": "string" + } + }, + "snowplow__campaigns_to_include": { + "recommendFullRefresh": true, + "order": 65, + "consoleGroup": "advanced", + "type": "array", + "description": "> Click the plus sign to add a new entry", + "minItems": 0, + "title": "Campaigns to Include", + "longDescription": "List of campaigns to include in the analysis (empty to keep all campaigns).", + "packageDefault": "[ ] (no filter applied)", + "group": "Contexts, Filters, and Logs", + "items": { + "type": "string" + } + }, + "snowplow__conversion_hosts": { + "recommendFullRefresh": true, + "order": 74, + "consoleGroup": "advanced", + "type": "array", + "description": "> Click the plus sign to add a new entry", + "minItems": 0, + "title": "URL Hosts", + "longDescription": "`url_hosts` to filter to in the data processing", + "packageDefault": "[] (no filter applied)", + "group": "Contexts, Filters, and Logs", + "items": { + "type": "string" + } + }, + "snowplow__consider_intrasession_channels": { + "recommendFullRefresh": true, + "order": 72, + "consoleGroup": "advanced", + "type": "boolean", + "group": "Contexts, Filters, and Logs", + "longDescription": "If `false`, only considers the channel at the start of the session (i.e. first page view). If `true`, considers multiple channels in the conversion session as well as historically.", + "packageDefault": "true", + "title": "Consider Intrasession Channels" + }, + "snowplow__spend_source": { + "recommendFullRefresh": true, + "order": 124, + "consoleGroup": "advanced", + "title": "Spend Source", + "description": "Source of marketing spend table", + "longDescription": "The source (schema and table) of your marketing spend source. Optional, needed for the ROAS calculation of the snowplow_attribution_overview. Should be changed to a table reference with `spend` by `channel` and/or `campaign` by `spend_tstamp` (which denotes a timestamp field) information.", + "type": "string", + "packageDefault": "not defined", + "group": "Warehouse and Tracker" + }, + "snowplow__conversion_stitching": { + "recommendFullRefresh": true, + "order": 77, + "consoleGroup": "advanced", + "type": "boolean", + "group": "Contexts, Filters, and Logs", + "longDescription": "This should be set to true if both the snowplow__conversion_stitching and snowplow__view_stitching variables are also enabled in the Unified package. If allowed it will consider the stitched_user_id field, not the user_identifier in the source data for more accurate results.", + "packageDefault": "false", + "title": "Conversion Stitching" + }, + "snowplow__conversion_clause": { + "recommendFullRefresh": true, + "order": 73, + "consoleGroup": "advanced", + "type": "string", + "title": "Conversions Clause", + "group": "Operation and Logic", + "longDescription": "A string of sql to filter on certain conversion events.", + "packageDefault": "cv_value > 0 and ev.user_identifier is not null" + }, + "snowplow__attribution_list": { + "recommendFullRefresh": false, + "order": 61, + "consoleGroup": "advanced", + "type": "array", + "description": "> Click the plus sign to add a new entry", + "minItems": 1, + "title": "Attribution List", + "longDescription": "List of attribution types to use for reporting. Can be at least one of: first_touch, last_touch, linear, position_based).", + "packageDefault": "['first_touch', 'last_touch', 'linear', 'position_based']", + "group": "Contexts, Filters, and Logs", + "items": { + "type": "string" + } + }, + "snowplow__attribution_start_date": { + "recommendFullRefresh": true, + "order": 7, + "consoleGroup": "required", + "type": "string", + "format": "date", + "title": "Attribution Start Date", + "group": "Operation and Logic", + "longDescription": "The date to start processing events from in the package on first run or a full refresh, based on the cv_tstamp (conversion timestamp).", + "packageDefault": "2023-01-01", + "description": "The date to start processing events from in the package on first run or a full refresh, based on `cv_tstamp`" + }, + "snowplow__enable_paths_to_non_conversion": { + "recommendFullRefresh": false, + "order": 101, + "consoleGroup": "advanced", + "type": "boolean", + "group": "Contexts, Filters, and Logs", + "longDescription": "If `true`, enable the paths_to_non_conversion model, which is a drop and recompute table that may be needed for more in-depth attribution analysis (used in the `path_summary` table as well)", + "packageDefault": "false", + "title": "Enable Paths To Non Conversion" + }, + "snowplow__enable_attribution_overview": { + "recommendFullRefresh": false, + "order": 102, + "consoleGroup": "advanced", + "type": "boolean", + "group": "Contexts, Filters, and Logs", + "longDescription": "If `true`, enable the attribution_overview model, which is a view that creates a great source for BI tools to use for reporting", + "packageDefault": "true", + "title": "Enable Attribution Overview" + }, + "snowplow__dev_target_name": { + "recommendFullRefresh": false, + "order": 87, + "consoleGroup": "advanced", + "type": "string", + "title": "Dev Target", + "description": "Target name of your development environment as defined in your `profiles.yml` file", + "longDescription": "The [target name](https://docs.getdbt.com/docs/core/connect-data-platform/profiles.yml) of your development environment as defined in your `profiles.yml` file. See the [Manifest Tables](/docs/modeling-your-data/modeling-your-data-with-dbt/package-mechanics/manifest-tables/) section for more details.", + "packageDefault": "dev", + "group": "Warehouse and Tracker" + }, + "snowplow__allow_refresh": { + "recommendFullRefresh": true, + "order": 39, + "consoleGroup": "advanced", + "type": "boolean", + "title": "Allow Refresh", + "group": "Operation and Logic", + "longDescription": "Used as the default value to return from the `allow_refresh()` macro. This macro determines whether the manifest tables can be refreshed or not, depending on your environment. See the [Manifest Tables](/docs/modeling-your-data/modeling-your-data-with-dbt/package-mechanics/manifest-tables/) section for more details.", + "packageDefault": "false" + } + } +} From abdc6732dac96b47293c5824950a136c2db6b9ee Mon Sep 17 00:00:00 2001 From: Enes Aldemir <88285759+spenes@users.noreply.github.com> Date: Wed, 16 Apr 2025 16:48:02 +0300 Subject: [PATCH 03/18] Enrich 5.3.0 (#1173) * Enrich 5.3.0 --- .../writing/index.md | 19 ++------ .../pii-pseudonymization-enrichment/index.md | 13 ++++-- .../enrichments/filtering-bot-events/index.md | 10 +++++ docs/reusable/discarding-events/_index.md | 43 +++++++++++++++++++ src/componentVersions.js | 4 +- 5 files changed, 69 insertions(+), 20 deletions(-) create mode 100644 docs/pipeline/enrichments/filtering-bot-events/index.md create mode 100644 docs/reusable/discarding-events/_index.md diff --git a/docs/pipeline/enrichments/available-enrichments/custom-javascript-enrichment/writing/index.md b/docs/pipeline/enrichments/available-enrichments/custom-javascript-enrichment/writing/index.md index 15a66ccf7d..7627491bcf 100644 --- a/docs/pipeline/enrichments/available-enrichments/custom-javascript-enrichment/writing/index.md +++ b/docs/pipeline/enrichments/available-enrichments/custom-javascript-enrichment/writing/index.md @@ -243,25 +243,12 @@ You might be tempted to update derived entities in a similar way by using `event ## Discarding the event -Sometimes you don’t want the event to appear in your data warehouse or lake, e.g. because you suspect it comes from a bot and not a real user. In this case, you can `throw` an exception in your JavaScript code, which will send the event to [failed events](/docs/fundamentals/failed-events/index.md): +```mdx-code-block +import DiscardingEvents from "@site/docs/reusable/discarding-events/_index.md" -```js -const botPattern = /.*Googlebot.*/; - -function process(event) { - const useragent = event.getUseragent(); - - if (useragent !== null && botPattern.test(useragent)) { - throw "Filtered event produced by Googlebot"; - } -} + ``` -:::caution - -This will create an “enrichment failure” failed event, which may be tricky to distinguish from genuine failures in your enrichment code, e.g. due to a mistake. In the future, we might provide a better mechanism for discarding events. - -::: ## Accessing Java methods diff --git a/docs/pipeline/enrichments/available-enrichments/pii-pseudonymization-enrichment/index.md b/docs/pipeline/enrichments/available-enrichments/pii-pseudonymization-enrichment/index.md index a06205d01d..4aa316f2a4 100644 --- a/docs/pipeline/enrichments/available-enrichments/pii-pseudonymization-enrichment/index.md +++ b/docs/pipeline/enrichments/available-enrichments/pii-pseudonymization-enrichment/index.md @@ -12,7 +12,7 @@ In Europe the obligations regarding Personal Data handling have been outlined on ## Configuration -- [Schema](https://github.com/snowplow/iglu-central/blob/master/schemas/com.snowplowanalytics.snowplow.enrichments/pii_enrichment_config/jsonschema/2-0-0) +- [Schema](https://github.com/snowplow/iglu-central/blob/master/schemas/com.snowplowanalytics.snowplow.enrichments/pii_enrichment_config/jsonschema/2-0-1) - [Example](https://github.com/snowplow/enrich/blob/master/config/enrichments/pii_enrichment_config.json) ```mdx-code-block @@ -23,7 +23,7 @@ import TestingWithMicro from "@site/docs/reusable/test-enrichment-with-micro/_in Two types of fields can be configured to be hashed: -- `pojo`: field that is effectively a scalar field in the enriched event (full list of fields that can be pseudonymized [here](https://github.com/snowplow/iglu-central/blob/master/schemas/com.snowplowanalytics.snowplow.enrichments/pii_enrichment_config/jsonschema/2-0-0#L43-L60)) +- `pojo`: field that is effectively a scalar field in the enriched event (full list of fields that can be pseudonymized [here](https://github.com/snowplow/iglu-central/blob/master/schemas/com.snowplowanalytics.snowplow.enrichments/pii_enrichment_config/jsonschema/2-0-1#L43-L60)) - `json`: field contained inside a self-describing JSON (e.g. in `unstruct_event`) With the configuration example, the fields `user_id` and `user_ipaddress` of the enriched event would be hashed, as well as the fields `email` and `ip_opt` of the unstructured event in case its schema matches _iglu:com.mailchimp/subscribe/jsonschema/1-\*-\*_. @@ -42,9 +42,16 @@ It's **important** to keep these things in mind when using this enrichment: - Hashing a field can change its format (e.g. email) and its length, thus making a whole valid original event invalid if its schema is not compatible with the hashing. - When updating the `salt` after it has already been used, same original values hashed with previous and new salt will have different hashes, thus making a join impossible and/or creating duplicate values. +### `anonymousOnly` mode +Enrich 5.3.0 introduced the `anonymousOnly` mode. When [anonymousOnly](https://github.com/snowplow/iglu-central/blob/master/schemas/com.snowplowanalytics.snowplow.enrichments/pii_enrichment_config/jsonschema/2-0-1#L155) is set to true, PII fields are masked only in events tracked in anonymous mode (i.e. the `SP-Anonymous` header is present). + +This is useful for compliance with regulation such as GDPR, where you would start with [anonymous tracking](/docs/sources/trackers/javascript-trackers/web-tracker/anonymous-tracking/index.md) by default (all identifiers are masked) and switch to non-anonymous tracking when the user consents to data collection (all identifiers are kept). + +By default, `anonymousOnly` is `false`, i.e. PII fields are always masked. + ## Input -[These fields](https://github.com/snowplow/iglu-central/blob/master/schemas/com.snowplowanalytics.snowplow.enrichments/pii_enrichment_config/jsonschema/2-0-0#L43-L60) of the enriched event and any field of an unstructured event or context can be hashed. +[These fields](https://github.com/snowplow/iglu-central/blob/master/schemas/com.snowplowanalytics.snowplow.enrichments/pii_enrichment_config/jsonschema/2-0-1#L43-L60) of the enriched event and any field of an unstructured event or context can be hashed. ## Output diff --git a/docs/pipeline/enrichments/filtering-bot-events/index.md b/docs/pipeline/enrichments/filtering-bot-events/index.md new file mode 100644 index 0000000000..9ea9c7beab --- /dev/null +++ b/docs/pipeline/enrichments/filtering-bot-events/index.md @@ -0,0 +1,10 @@ +--- +title: "Filtering bot events" +sidebar_position: 100 +--- + +```mdx-code-block +import DiscardingEvents from "@site/docs/reusable/discarding-events/_index.md" + + +``` \ No newline at end of file diff --git a/docs/reusable/discarding-events/_index.md b/docs/reusable/discarding-events/_index.md new file mode 100644 index 0000000000..b31854f831 --- /dev/null +++ b/docs/reusable/discarding-events/_index.md @@ -0,0 +1,43 @@ +Sometimes you don’t want the event to appear in your data warehouse or lake, e.g. because you suspect it comes from a bot and not a real user. + +Starting with Enrich 5.3.0, it is possible to drop an event by calling `event.drop()` in JavaScript enrichment code: + +```js +const botPattern = /.*Googlebot.*/; + +function process(event) { + const useragent = event.getUseragent(); + + if (useragent !== null && botPattern.test(useragent)) { + event.drop(); + } +} +``` + +This mechanism can be used to drop not only good events, but also invalid events. The dropped events will not be sent to any stream or destination, thus lowering the infrastructure costs. + +:::caution + +There is no way to recover dropped events therefore use it with caution. + +::: + +Another way to discard events is throwing an exception in your JavaScript code, which will send the event to [failed events](/docs/fundamentals/failed-events/index.md): + +```js +const botPattern = /.*Googlebot.*/; + +function process(event) { + const useragent = event.getUseragent(); + + if (useragent !== null && botPattern.test(useragent)) { + throw "Filtered event produced by Googlebot"; + } +} +``` + +:::caution + +This will create an “enrichment failure” failed event, which may be tricky to distinguish from genuine failures in your enrichment code, e.g. due to a mistake. + +::: diff --git a/src/componentVersions.js b/src/componentVersions.js index 35d363f426..2be570b673 100644 --- a/src/componentVersions.js +++ b/src/componentVersions.js @@ -21,8 +21,10 @@ export const versions = { webViewTracker: '0.3.0', // Core pipeline + collector: '3.3.0', + enrich: '5.3.0', collector: '3.4.0', - enrich: '5.2.0', + enrich: '5.3.0', sqs2kinesis: '1.0.4', dataflowRunner: '0.7.5', snowbridge: '3.1.1', From 009ffb85dbb5f3852e5ee3eb17995a082bb7564b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 22 Apr 2025 10:17:59 +0100 Subject: [PATCH 04/18] [create-pull-request] automated change (#1221) --- src/dbtVersions.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/dbtVersions.js b/src/dbtVersions.js index 53de276db5..f4bc7810ae 100644 --- a/src/dbtVersions.js +++ b/src/dbtVersions.js @@ -41,6 +41,13 @@ export const dbtVersions = { "dbt-labs/dbt_utils": ">=1.0.0 <2.0.0", "snowplow/snowplow_utils": ">=0.17.0 <0.18.0" } + }, + "0.5.0": { + "dbtversion": ">=1.6.0 <2.0.0", + "packages": { + "dbt-labs/dbt_utils": ">=1.0.0 <2.0.0", + "snowplow/snowplow_utils": ">=0.17.0 <0.18.0" + } } }, "snowplow/snowplow_ecommerce": { From e4c5457f6fcb31ee92fc80cc9581706fa242a378 Mon Sep 17 00:00:00 2001 From: Oguzhan Unlu Date: Tue, 22 Apr 2025 13:03:00 +0300 Subject: [PATCH 05/18] Upgrade iglu server to 0.14.1 (#1223) --- src/componentVersions.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/componentVersions.js b/src/componentVersions.js index 2be570b673..b0e8c40fb3 100644 --- a/src/componentVersions.js +++ b/src/componentVersions.js @@ -70,7 +70,7 @@ export const versions = { analyticsSdkScala: '3.0.0', // Iglu - igluServer: '0.14.0', + igluServer: '0.14.1', igluctl: '0.13.0', igluObjCClient: '0.1.1', igluRubyClient: '0.2.0', From 3133dcba40e6489bbc60fdff472c86dc5a790b5a Mon Sep 17 00:00:00 2001 From: Jethro Nederhof Date: Tue, 29 Apr 2025 17:35:30 +1000 Subject: [PATCH 06/18] Document JS element tracking plugin (#1224) * Document JS element tracking plugin * Move element tracking examples higher * Vale lint --- .github/styles/Snowplow/Acronyms.yml | 1 + .github/styles/Snowplow/Headings.yml | 1 + .../web-tracker/plugins/index.md | 1 + .../tracking-events/element-tracking/index.md | 816 ++++++++++++++++++ src/remark/abbreviations.js | 1 + 5 files changed, 820 insertions(+) create mode 100644 docs/sources/trackers/javascript-trackers/web-tracker/tracking-events/element-tracking/index.md diff --git a/.github/styles/Snowplow/Acronyms.yml b/.github/styles/Snowplow/Acronyms.yml index d386c1dae4..b2111bc7d7 100644 --- a/.github/styles/Snowplow/Acronyms.yml +++ b/.github/styles/Snowplow/Acronyms.yml @@ -10,6 +10,7 @@ second: '(?:\b[A-Z][a-z]+ )+\(([A-Z]{3,5})\)' exceptions: - API - ASP + - CDN - CLI - CPU - CSS diff --git a/.github/styles/Snowplow/Headings.yml b/.github/styles/Snowplow/Headings.yml index 6fbbe9abaa..de4fc93f7e 100644 --- a/.github/styles/Snowplow/Headings.yml +++ b/.github/styles/Snowplow/Headings.yml @@ -9,6 +9,7 @@ exceptions: - CLI - Cosmos - Docker + - DOM - Emmet - gRPC - I diff --git a/docs/sources/trackers/javascript-trackers/web-tracker/plugins/index.md b/docs/sources/trackers/javascript-trackers/web-tracker/plugins/index.md index 2c1dac5a03..76426af7a4 100644 --- a/docs/sources/trackers/javascript-trackers/web-tracker/plugins/index.md +++ b/docs/sources/trackers/javascript-trackers/web-tracker/plugins/index.md @@ -30,6 +30,7 @@ If you are using the JavaScript tracker with the full `sp.js` and your plugin is | [Ecommerce (Snowplow)](/docs/sources/trackers/javascript-trackers/web-tracker/tracking-events/ecommerce/index.md) | Events and entities | Manual | ✅ | ❌ | `browser-plugin-snowplow-ecommerce` | | [Ecommerce (Enhanced)](/docs/sources/trackers/javascript-trackers/web-tracker/tracking-events/ecommerce/enhanced/index.md) | Events | Manual | ❌ | ❌ | `browser-plugin-enhanced-ecommerce` | | [Errors](/docs/sources/trackers/javascript-trackers/web-tracker/tracking-events/errors/index.md) | Events | Manual and automatic | ✅ | ❌ | `browser-plugin-error-tracking` | +| [Element visibility](/docs/sources/trackers/javascript-trackers/web-tracker/tracking-events/element-tracking/index.md) | Events | Automatic | ❌ | ❌ | `browser-plugin-element-tracking` | | [Event Specifications](/docs/sources/trackers/javascript-trackers/web-tracker/tracking-events/event-specifications/index.md) | Entities | Automatic | ❌ | ❌ | `browser-plugin-event-specifications` | | [Forms](/docs/sources/trackers/javascript-trackers/web-tracker/tracking-events/form-tracking/index.md) | Events | Automatic | ✅ | ❌ | `browser-plugin-form-tracking` | | [GA cookies](/docs/sources/trackers/javascript-trackers/web-tracker/tracking-events/ga-cookies/index.md) | Entities | Automatic | ✅ | ❌ | `browser-plugin-ga-cookies` | diff --git a/docs/sources/trackers/javascript-trackers/web-tracker/tracking-events/element-tracking/index.md b/docs/sources/trackers/javascript-trackers/web-tracker/tracking-events/element-tracking/index.md new file mode 100644 index 0000000000..e89d32da96 --- /dev/null +++ b/docs/sources/trackers/javascript-trackers/web-tracker/tracking-events/element-tracking/index.md @@ -0,0 +1,816 @@ +--- +title: "Element tracking" +sidebar_position: 55 +--- + +# Element visibility and lifecycle tracking + +```mdx-code-block +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; +``` + +Element tracking enables declarative tracking of page elements existing on web pages and scrolling into view. + +The plugin lets you define rules for which elements to track, and lets you trigger events for any combination of: + +- New matching elements get added to a page +- Existing elements get changed to match a rule +- Matching elements get scrolled into a user's view and become visible +- Matching elements get scrolled out of a user's view and become no longer visible +- Elements get changed to no longer match a rule +- Matching elements get removed from a page + +As a configuration-based plugin, you only need to define which elements should generate events, and in which scenarios. +You can reuse the same configuration for generic tracking across a varying number of pages or sites. + +Each event contains information about the matching element, and you can configure extra details to extract to allow dynamic event payloads. + +Example use cases for these events include: + +- Funnel steps (form on page > form in view > [form tracking events](/docs/sources/trackers/javascript-trackers/web-tracker/tracking-events/form-tracking/index.md)) +- List impression tracking (product impressions) +- Component performance (recommendations performance, newsletter sign-up forms, modal popups) +- Product usage (elements that appear on-hover, labeling or grouping events related to specific features) +- Advertisement impression tracking + +## Install plugin + + + + +| Tracker Distribution | Included | +|----------------------|----------| +| `sp.js` | ❌ | +| `sp.lite.js` | ❌ | + +**Download:** + +
Download from GitHub Releases (Recommended)GitHub Releases (plugins.umd.zip)
Available on jsDelivrjsDelivr (latest)
Available on unpkgunpkg (latest)
+ +:::note + +The links to the CDNs point to the current latest version. +You should pin to a specific version when integrating this plugin on your website if you are using a third-party CDN in production. + +::: + +
+ + +- `npm install @snowplow/browser-plugin-element-tracking` +- `yarn add @snowplow/browser-plugin-element-tracking` +- `pnpm add @snowplow/browser-plugin-element-tracking` + + +
+ +## Examples + +Here are some example rules for simple use cases on the [Snowplow website](https://snowplow.io/) ([snapshot at time of writing](https://web.archive.org/web/20250422013533/https://snowplow.io/)). + +The code examples use the [JavaScript Tracker syntax](/docs/sources/trackers/javascript-trackers/web-tracker/index.md), but should easily adapt to Browser Tracker syntax if needed. + +
+ Scroll sections + + The homepage has content grouped into distinct "layers" as you scroll down the page. + To see when users scroll down to each section, you can track an `expose` event for each section. + You can capture the header element text to identify each one. + + ```javascript title="Rule configuration" + snowplow('startElementTracking', { + elements: { + selector: "section", + expose: { when: "element" }, + details: { child_text: { title: "h2" } } + } + }); + ``` + + ```json title="Event: expose_event" + { + "schema": "iglu:com.snowplowanalytics.snowplow/element/jsonschema/1-0-0", + "data": { + "element_name": "section", + "width": 1920, + "height": 1111.7333984375, + "position_x": 0, + "position_y": 716.4500122070312, + "doc_position_x": 0, + "doc_position_y": 716.4500122070312, + "element_index": 2, + "element_matches": 10, + "originating_page_view": "06dbb0a2-9acf-4ae4-9562-1469b6d12c5d", + "attributes": [ + { + "source": "child_text", + "attribute": "title", + "value": "Why Data Teams Choose Snowplow" + } + ] + } + } + ``` + + ```json title="Event: expose_event" + { + "schema": "iglu:com.snowplowanalytics.snowplow/element/jsonschema/1-0-0", + "data": { + "element_name": "section", + "width": 1920, + "height": 2880, + "position_x": 0, + "position_y": 896.683349609375, + "doc_position_x": 0, + "doc_position_y": 1828.183349609375, + "element_index": 3, + "element_matches": 10, + "originating_page_view": "06dbb0a2-9acf-4ae4-9562-1469b6d12c5d", + "attributes": [ + { + "source": "child_text", + "attribute": "title", + "value": "How Does Snowplow Work?" + } + ] + } + } + ``` + +
+ +
+ Content depth + + The blog posts have longer-form content. + Snowplow's page ping events track scroll depth by pixels, but those measurements become inconsistent between devices and page. + To see how much content gets consumed, you can generate stats based on the paragraphs in the content. + You can also get periodic stats based on the entire article in page pings. + + ```javascript title="Rule configuration" + snowplow('startElementTracking', { + elements: [ + { + selector: ".blogs_blog-post-body_content", + name: "blog content", + expose: false, + includeStats: ["page_ping"] + }, + { + selector: ".blogs_blog-post-body_content p", + name: "blog paragraphs" + } + ] + }); + ``` + + Because the expose event contains the `element_index` and `element_matches`, you can easily query the largest `element_index` by page view ID. + The result tells you consumption statistics for individual views of each article. + You can then summarize that metric to the content or category level, or converted to a percentage by comparing with `element_matches`. + + ```json title="Event: expose_event" + { + "schema": "iglu:com.snowplowanalytics.snowplow/contexts/jsonschema/1-0-0", + "data": [ + { + "schema": "iglu:com.snowplowanalytics.snowplow/element/jsonschema/1-0-0", + "data": { + "element_name": "blog paragraphs", + "width": 800, + "height": 48, + "position_x": 320, + "position_y": 533.25, + "doc_position_x": 320, + "doc_position_y": 1373, + "element_index": 6, + "element_matches": 24, + "originating_page_view": "f390bec5-f63c-48af-b3ad-a03f0511af7f", + "attributes": [] + } + }, + { + "schema": "iglu:com.snowplowanalytics.snowplow/web_page/jsonschema/1-0-0", + "data": { + "id": "f390bec5-f63c-48af-b3ad-a03f0511af7f" + } + } + ] + } + ``` + + The periodic page ping events also give you a summary of the total progress in the `max_y_depth_ratio`/`max_y_depth` values. + With `y_depth_ratio` you can also see when users backtrack up the page. + + ```json title="Event: page_ping" + { + "schema": "iglu:com.snowplowanalytics.snowplow/element_statistics/jsonschema/1-0-0", + "data": { + "element_name": "blog content", + "element_index": 1, + "element_matches": 1, + "current_state": "unknown", + "min_size": "800x3928", + "current_size": "800x3928", + "max_size": "800x3928", + "y_depth_ratio": 0.20302953156822812, + "max_y_depth_ratio": 0.4931262729124236, + "max_y_depth": "1937/3928", + "element_age_ms": 298379, + "times_in_view": 0, + "total_time_visible_ms": 0 + } + } + ``` + +
+ +
+ Simple funnels + + A newsletter sign-up form exists at the bottom of the page. + Performance measurement becomes difficult because many visitors don't even see it. + To test this you first need to know: + + - When the form exists on a page + - When the form is actually seen + - When people actually interact with the form + - When the form is finally submitted + + The form tracking plugin can only do the last parts, but the element tracker gives you the earlier steps. + If you end up adding more forms in the future, you'll want to know which is which, so you can mark the footer as a component so you can split it out later. + + ```javascript title="Rule configuration" + snowplow('startElementTracking', { + elements: [ + { + selector: ".hbspt-form", + name: "newsletter signup", + create: true, + }, + { + selector: "footer", + component: true, + expose: false + } + ] + }); + ``` + + If you try this on a blog page, you actually get two `create_element` events. + Blog posts have a second newsletter sign-up in a sidebar next to the content. + Because only the second form is a member of the `footer` component, you can easily see which one you are trying to measure when you query the data later. + + ```json title="Event: create_element" + { + "schema": "iglu:com.snowplowanalytics.snowplow/contexts/jsonschema/1-0-0", + "data": [ + { + "schema": "iglu:com.snowplowanalytics.snowplow/element/jsonschema/1-0-0", + "data": { + "element_name": "newsletter signup", + "width": 336, + "height": 161, + "position_x": 1232, + "position_y": 238.88333129882812, + "doc_position_x": 1232, + "doc_position_y": 3677.883331298828, + "element_index": 1, + "element_matches": 2, + "originating_page_view": "02e30714-a84a-42f8-8b07-df106d669db0", + "attributes": [] + } + }, + { + "schema": "iglu:com.snowplowanalytics.snowplow/web_page/jsonschema/1-0-0", + "data": { + "id": "02e30714-a84a-42f8-8b07-df106d669db0" + } + } + ] + } + ``` + + ```json title="Event: create_element" + { + "schema": "iglu:com.snowplowanalytics.snowplow/contexts/jsonschema/1-0-0", + "data": [ + { + "schema": "iglu:com.snowplowanalytics.snowplow/element/jsonschema/1-0-0", + "data": { + "element_name": "newsletter signup", + "width": 560, + "height": 137, + "position_x": 320, + "position_y": 1953.5, + "doc_position_x": 320, + "doc_position_y": 5392.5, + "element_index": 2, + "element_matches": 2, + "originating_page_view": "02e30714-a84a-42f8-8b07-df106d669db0", + "attributes": [] + } + }, + { + "schema": "iglu:com.snowplowanalytics.snowplow/component_parents/jsonschema/1-0-0", + "data": { + "element_name": "newsletter signup", + "component_list": [ + "footer" + ] + } + }, + { + "schema": "iglu:com.snowplowanalytics.snowplow/element/jsonschema/1-0-0", + "data": { + "element_name": "footer", + "width": 1920, + "height": 1071.5, + "position_x": 0, + "position_y": 1212, + "doc_position_x": 0, + "doc_position_y": 4651, + "originating_page_view": "", + "attributes": [] + } + }, + { + "schema": "iglu:com.snowplowanalytics.snowplow/web_page/jsonschema/1-0-0", + "data": { + "id": "02e30714-a84a-42f8-8b07-df106d669db0" + } + } + ] + } + ``` + +
+ +
+ Recommendations performance + + The homepage contains a section for the "Latest Blogs from Snowplow." + This could represent recommendations or some other form of personalization. + If it did, one might want to optimize it. + Link tracking could tell you when a recommendation worked and a visitor clicked it, but how would identify the recommendation not encouraging clicks? + If you track when the widget becomes visible and include the items that got recommended, you could correlate that with the clicks to measure performance. + For fairer measurement of visibility, you can configure that visibility only counts if at least 50% is in view, and it has to be on screen for at least 1.5 seconds. + You'll also collect the post title and author information. + + + ```javascript title="Rule configuration" + snowplow('startElementTracking', { + elements: [ + { + selector: ".blog_list-header_list-wrapper", + name: "recommended_posts", + create: true, + expose: { when: "element", minTimeMillis: 1500, minPercentage: 0.5 }, + contents: [ + { + selector: ".collection-item", + name: "recommended_item", + details: { child_text: { title: "h3", author: ".blog_list-header_author-text > p" } } + } + ] + } + ] + }); + ``` + + Scrolling down to see the items and you see the items that get served to the visitor: + + ```json title="Event: expose_element" + { + "schema": "iglu:com.snowplowanalytics.snowplow/contexts/jsonschema/1-0-0", + "data": [ + { + "schema": "iglu:com.snowplowanalytics.snowplow/element/jsonschema/1-0-0", + "data": { + "element_name": "recommended_posts", + "width": 1280, + "height": 680.7666625976562, + "position_x": 320, + "position_y": 437.70001220703125, + "doc_position_x": 320, + "doc_position_y": 6261.066711425781, + "element_index": 1, + "element_matches": 1, + "originating_page_view": "034db1d6-1d60-42ca-8fe1-9aafc0442a22", + "attributes": [] + } + }, + { + "schema": "iglu:com.snowplowanalytics.snowplow/element_content/jsonschema/1-0-0", + "data": { + "element_name": "recommended_item", + "parent_name": "recommended_posts", + "parent_position": 1, + "position": 1, + "attributes": [ + { + "source": "child_text", + "attribute": "title", + "value": "Data Pipeline Architecture Patterns for AI: Choosing the Right Approach" + }, + { + "source": "child_text", + "attribute": "author", + "value": "Matus Tomlein" + } + ] + } + }, + { + "schema": "iglu:com.snowplowanalytics.snowplow/element_content/jsonschema/1-0-0", + "data": { + "element_name": "recommended_item", + "parent_name": "recommended_posts", + "parent_position": 1, + "position": 2, + "attributes": [ + { + "source": "child_text", + "attribute": "title", + "value": "Data Pipeline Architecture For AI: Why Traditional Approaches Fall Short" + }, + { + "source": "child_text", + "attribute": "author", + "value": "Matus Tomlein" + } + ] + } + }, + { + "schema": "iglu:com.snowplowanalytics.snowplow/element_content/jsonschema/1-0-0", + "data": { + "element_name": "recommended_item", + "parent_name": "recommended_posts", + "parent_position": 1, + "position": 3, + "attributes": [ + { + "source": "child_text", + "attribute": "title", + "value": "Agentic AI Applications: How They Will Turn the Web Upside Down" + }, + { + "source": "child_text", + "attribute": "author", + "value": "Yali\tSassoon" + } + ] + } + }, + { + "schema": "iglu:com.snowplowanalytics.snowplow/web_page/jsonschema/1-0-0", + "data": { + "id": "034db1d6-1d60-42ca-8fe1-9aafc0442a22" + } + } + ] + } + ``` + +
+ +## Enable element tracking + +You can begin tracking elements by providing configuration to the plugin's `startElementTracking` method: + + + + +```javascript +window.snowplow('addPlugin', + "https://cdn.jsdelivr.net/npm/@snowplow/browser-plugin-element-tracking@latest/dist/index.umd.min.js", + ["snowplowElementTracking", "SnowplowElementTrackingPlugin"] +); + +snowplow('startElementTracking', { elements: [/* configuration */] }); +``` + + + + +Element tracking is part of a separate plugin, `@snowplow/browser-plugin-element-tracking`. You need to install it with your favorite package manager: `npm install @snowplow/browser-plugin-element-tracking` and then initialize it: + +```javascript +import { newTracker } from '@snowplow/browser-tracker'; +import { SnowplowElementTrackingPlugin, startElementTracking } from '@snowplow/browser-plugin-element-tracking'; + +newTracker('sp1', '{{collector_url}}', { + appId: 'my-app-id', + plugins: [ SnowplowElementTrackingPlugin() ], +}); + +startElementTracking({ elements: [/* configuration */] }); +``` + + + +Each use of this method adds the given list of element rules to the plugin configuration to start automatically tracking events. + +The `elements` configuration can take a single rule or an array of rules. + +Beyond `elements`, you can also specify `context`: an array of static entities or entity-generating functions to include custom information with all events generated by the plugin. +This can also exist at the individual rule level for more specific entity requirements. + +For the specifics of rule configuration, see [Rule configuration](#rule-configuration) below. + +## Disabling element tracking + +To turn off tracking, use `endElementTracking` to remove the rule configuration. +Providing no options to `endElementTracking` removes all earlier configured rules. +If all rules get removed, the plugin removes its listeners until new rules get configured. + +If you want to stop tracking based for specific rules, you can provide the `name` or `id` values to the `endElementTracking` method. +Each rule provided to `startElementTracking` gets associated with a `name` - and optionally, an `id`. +If you don't specify a `name`, the `name` defaults to the `selector` value (required for all rules). + +For more complex requirements, you can also specify a callback function to decide if a rule should turn off (callback returns `true`) or not (callback returns `false`). + + + + +```javascript +snowplow('endElementTracking', { elements: ['name1', 'name2'] }); // removes based on `name` matching; multiple rules may share a name +snowplow('endElementTracking', { elementIds: ['id1'] }); // removes rules based on `id` matching; at most one rule can have the same `id` +snowplow('endElementTracking', { filter: (rule) => /recommendations/i.test(rule.name) }); // more complicated matching; rules where the `filter` function returns true will be removed +snowplow('endElementTracking'); // remove all configured rules +``` + + + + +```javascript +endElementTracking({ elements: ['name1', 'name2'] }); // removes based on `name` matching; multiple rules may share a name +endElementTracking({ elementIds: ['id1'] }); // removes rules based on `id` matching; at most one rule can have the same `id` +endElementTracking({ filter: (rule) => /recommendations/i.test(rule.name) }); // more complicated matching; rules where the `filter` function returns true will be removed +endElementTracking(); // remove all configured rules +``` + + + +Removing rules by name removes all rules with matching names - rule names don't require uniqueness. +Rule IDs _must be_ unique, so only a single rule matches per `elementIds` value. +If you specify more than one of the `elementIds`, `elements`, and `filter` options, they get evaluated in that order. +Passing an empty object to `endElementTracking` counts as specifying no options - and removes no rules - which differs to calling it with no arguments. + +## Rule configuration + +When calling `startElementTracking`, you specify the `elements` option with either a single rule or an array of rules. +Each rule defines core information like: the elements to match, events to fire, extra details to collect about each element (or their contents), and custom entities to attach. + +### Core configuration + +The foundational configuration required for working with the plugin APIs. + +| Rule property | Type | Description | Status | +|---------------|------|-------------|----------| +| `selector`|`string`|A CSS selector string that matches one or more elements on the page that should trigger events from this rule.|**Required**| +| `name`|`string`|A label to name this rule. Allows you to keep a stable name for events generated by this rule, even if the `selector` changes, so the data produced remains consistent. You can share a single `name` between many rules to have different configurations for different selectors. If not supplied, the `selector` value becomes the `name`.|_Recommended_| +| `id`|`string`|A specific identifier for this rule. Useful if you share a `name` between many rules and need to specifically remove individual rules within that group.|Optional| + +### Event configuration + +These settings define which events should automatically fire, and the situations when they should occur. +By default, only the `expose` setting gets enabled, so the plugin tracks when elements matching the rule's `selector` become visible on the user's viewport. +For convenience, each option can use a boolean to turn on or off each event type for elements matching the selector. +You can also use an object to have more control on when the events get triggered. + +| Rule property | Type | Description | Default | +|---------------|------|-------------|----------| +| `create`|`boolean` or `object`|Controls firing `element_create` events when the element gets added to the page (or already exists when the rule gets configured).|`false`| +| `destroy`|`boolean` or `object`|Controls firing `element_destroy` events when the element gets removed from the page.|`false`| +| `expose`|`boolean` or `object`|Controls firing `element_expose` events when the element becomes visible in the user's viewport.|`true`| +| `obscure`|`boolean` or `object`|Controls firing `element_obscure` events when the element becomes no longer visible in the user's viewport.|`false`| + +#### General event options + +These common options are available for the `create`, `destroy`, `expose`, and `obscure` settings and allow limiting how often the event fires. + +| Rule property | Type | Description | Status | +|---------------|------|-------------|----------| +| `when`|`string` or `object`|Sets the limit on how many times the event should fire for matched elements.|**Required**| +| `condition`|`array`|A single or list of many [data selectors](#data-selectors); if the final result has no elements the event won't trigger.|Optional| + +For `when`, the available options include, in descending order of frequency: + +- `always`: generate an event every time an element becomes eligible (for example, every time an event becomes visible) +- `element`: only fire 1 event for each specific element that matches the rule for the lifetime of the rule (for example, just the first time each element becomes visible) +- `pageview`: like `element`, but reset the state when the tracker next tracks a page view event; this can be useful for single page applications where the plugin may have a long lifetime but you still want to limit the number of events +- `once`: only fire 1 event _per rule_, so even if there are many elements matching `selector` only track the first time this occurs +- `never`: never track this event for this rule. This is useful for defining `components` + +When using the `boolean` shorthand, `true` is identical to `{ when: "always" }`, and `false` is `{ when: "never" }`. + +#### Expose event options + +As well as the [general event options](#general-event-options), `expose` has some extra options specific to its use case. + +| Rule property | Type | Description | +|---------------|------|-------------| +| `minPercentage`|`number`|For larger elements, only consider the element visible if at least this percentage of its area is visible.| +| `minTimeMillis`|`number`|Only consider the element visible if it's cumulative time on screen exceeds this value, in milliseconds.| +| `minSize`|`number`|Unless the elements area (height * width) is at least this size, don't consider the element as visible (for example, don't track empty elements).| +| `boundaryPixels`|`number` or `array`|Add this number of pixels to the dimensions (top, right, bottom, left) of the element when calculating its dimensions for `minPercentage` purposes. You can specify a single value, a pair for vertical and horizontal values, or specific values for each of top, right, bottom, and left.| + +### Shadow DOM compatibility + +If the elements you want to track exist within [shadow DOM](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_shadow_DOM) trees, the plugin may not identify them. +Use these settings to notify the plugin that it should descend into shadow hosts to identify elements to match the rule against. + +| Rule property | Type | Description | +|---------------|------|-------------| +| `shadowSelector`|`string`|A CSS selector for elements that are shadow hosts containing the actual `selector`-targeted elements.| +| `shadowOnly`|`boolean`|By default, the plugin matches `selector` elements both outside and inside shadow hosts matched by `shadowSelector`; set this to `true` to only match elements within shadow hosts matched by `shadowSelector`. (for example, you may want all `button` elements in a web component, but that selector is too generic when applied to your whole site, so this setting can limit the matches to only those within those shadow hosts).| + +### Element data + +These settings control extra information captured about the event generating the event. + +| Rule property | Type | Description | +|---------------|------|-------------| +| `component`|`boolean`|When `true`, defines these elements as being a component. Events generated by this rule, or other rules targeting their child elements, have this rules `name` attached via the `component_parents` entity, showing the component hierarchy that the element belongs to.| +| `details`|`array`|A list of [data selectors](#data-selectors) of information to capture about this element. The selected values populate the `attributes` object in the [`element` entity](#events-and-entities).| +| `includeStats`|`array`|An array of `event_name` values that the plugin should attach an [`element_statistics` entity](#element-statistics) to.| +| `contents`|`array`|You can nest configurations in this property to collect data about elements nested within the elements matched by this rule (for example, this rule could target a recommendations widget, and using `contents` you could describe the individual recommendations served within it). The nested configurations can not trigger their own events, and their [event configuration](#event-configuration) gets ignored. Nested `details` work for the extra `element_content` entities that get generated (based on the nested `name`), and you can further nest `contents` arbitrarily, though you may end up with a large number of entities.| + +## Data selectors + +Data selectors are a declarative way to extract information from elements matched by rules. + +The plugin uses data selectors when deciding if an element should trigger an event (using [`condition`](#general-event-options)), or when building the `element` entity's `attributes` property based on a rule's [`details` and `contents` settings](#element-data). + +The declarative configuration lets you safely extract information without having to explicitly write code, or still get information where callbacks aren't possible. +For example, a function defined in Google Tag Manager that passes through a Tag Template can not work with DOM elements directly, which limits the data it could extract. + +The declarative use is optional, and you can also just provide a callback function that accepts an element and returns an object if you prefer. + +You define data selectors as a list, so you can also combine the two approaches. +When evaluating each list of data selectors, the result is a list of triplets describing: + +1. The `source`/type of the data selector +2. The selected `attribute` name +3. The selected attribute `value` + +Each data selector should be a function or an object with any of the following properties: + +| Data selector property | Value type | Description | +|---------------|------|-------------| +||`function`|A custom callback. The function should return an object with `string` properties, each of which produce a result in the output list. Values get cast to `string`; empty values (such as `undefined`) get skipped entirely.| +|`attributes`|`array`|Produces a result for each provided attribute extracted via the [`getAttribute()` API](https://developer.mozilla.org/en-US/docs/Web/API/Element/getAttribute). The value is often the initial value set in the HTML of the page, compared to `properties` which may be a more recent value.| +|`properties`|`array`|Produces a result for each provided property name sourced from the element.| +|`dataset`|`array`|Produces a result for each provided property name sourced from the element's dataset attribute. This should be the camel-case format version, rather than the attribute-style name.| +|`child_text`|`object`|The value should be an object mapping names to CSS selectors; produces a result for each name mapped to the `textContent` of the first matching child element. Be cautious of large text values.| +|`content`|`object`|The value should be an object mapping names to regular expression patterns. Each pattern gets evaluated against the `textContent` of the matching element and produces an attribute with the matched value. If the pattern contains matching groups, uses the first captured group.| +|`selector`|`boolean`|Attach the rule's `selector` as an attribute. Can be useful if you are sharing `names` between rules and need to know which rule matched.| +|`match`|`object`|The value should be an object mapping other attribute names to values or functions. The current set of attribute results get checked against this object; if no attributes have the same value (or the function doesn't return `true` for the value) then discard the current list of results. This can be useful for the `condition` setting.| + +The `source` matches the property used, or `callback` if a callback function is the source. +If the callback encounters an error, it produces an `error`-sourced value. + +For the purposes of `condition` matching, events don't fire if the resulting list of attributes is empty. + +## Events and entities + +Events generated by the plugin have simple payloads, consisting of an `element_name` property that's referenced by the entities attached to the event. + +The event schemas are: + +- [`create_element`](https://github.com/snowplow/iglu-central/blob/master/schemas/com.snowplowanalytics.snowplow/create_element/jsonschema/1-0-0) +- [`destroy_element`](https://github.com/snowplow/iglu-central/blob/master/schemas/com.snowplowanalytics.snowplow/destroy_element/jsonschema/1-0-0) +- [`expose_element`](https://github.com/snowplow/iglu-central/blob/master/schemas/com.snowplowanalytics.snowplow/expose_element/jsonschema/1-0-0) +- [`obscure_element`](https://github.com/snowplow/iglu-central/blob/master/schemas/com.snowplowanalytics.snowplow/obscure_element/jsonschema/1-0-0) + +These events include an `element` entity which includes: + +- An `element_name` matching the one in the event payload +- Size and position information +- Any attributes collected via the [`detail` setting](#element-data) + +```json reference title="Entity - iglu:com.snowplowanalytics.snowplow/element/jsonschema/1-0-0" referenceLinkText="See schema on GitHub" +https://github.com/snowplow/iglu-central/blob/master/schemas/com.snowplowanalytics.snowplow/element/jsonschema/1-0-0 +``` + +### Optional entities + +Depending on configuration, events may also include the following entities: + +#### Element statistics + +By using the [`includeStats` setting](#element-data), you can configure the plugin to attach this entity to any events sent to the tracker (including those not generated by this plugin). + +For each rule with this configured, entities for each matching element include: + +- Visibility state at the time of the event +- Smallest, largest, and current size +- Element-specific min/max scroll depth information +- Time since the element was first observed (element age) +- How many times the element has been in view +- Cumulative total time the element has been in view + +If the selector matches a lot of elements, this can enlarge event payload sizes, use caution with the `selector` used with this setting. + +Note that `includeStats` requires opt-in for all event types, even those generated by this plugin: + + + + +```javascript +snowplow('startElementTracking', { elements: { + selector: 'main.article', + name: 'article_content', + includeStats: ['expose_element', 'page_ping'] +} }); +``` + + + + +```javascript +import { SnowplowElementTrackingPlugin, startElementTracking } from '@snowplow/browser-plugin-element-tracking'; + +startElementTracking({ elements: { + selector: 'main.article', + name: 'article_content', + includeStats: ['expose_element', 'page_ping'] +} }); +``` + + + +Define non-self-describing events with the event names assigned to them during enrichment (`page_view`, `page_ping`, `event` (structured events), `transaction`, and `transaction_item`). + +```json reference title="Entity - iglu:com.snowplowanalytics.snowplow/element_statistics/jsonschema/1-0-0" referenceLinkText="See schema on GitHub" +https://github.com/snowplow/iglu-central/blob/master/schemas/com.snowplowanalytics.snowplow/element_statistics/jsonschema/1-0-0 +``` + +#### Component hierarchy + +If any configured rules are a [`component`](#element-data), events generated by the plugin may include the `component_parents` entity. + +This includes an `element_name` reference, and a `component_list` that's a list of any `component`-rule names that are ancestors of that element. +Use these values to aggregate events to different levels of a component hierarchy. + +The plugin also exposes a `getComponentListGenerator` command, that returns a function that accepts an element and returns this entity. +This function gets used to attach the entity to custom events, or events generated by other plugins like the [form](/docs/sources/trackers/javascript-trackers/web-tracker/tracking-events/form-tracking/index.md) or [link](/docs/sources/trackers/javascript-trackers/web-tracker/tracking-events/link-click/index.md) tracking plugins. + + + + +```javascript +snowplow('getComponentListGenerator', function (componentGenerator, componentGeneratorWithDetail) { + // access a context generator aware of the startElementTracking "components" configuration + // this will attach the component_parents entity to events generated by these plugins that show the component hierarchy + snowplow('enableLinkClickTracking', { context: [componentGenerator] }); + snowplow('enableFormTracking', { context: [componentGenerator] }); + + // componentGeneratorWithDetail will also populate element_detail entities for each component, but is not directly compatible with the above plugin APIs +}); +``` + + + + +```javascript +import { getComponentListGenerator } from '@snowplow/browser-plugin-element-tracking'; +import { enableLinkClickTracking } from '@snowplow/browser-plugin-link-click-tracking'; +import { enableFormTracking } from '@snowplow/browser-plugin-form-tracking'; + +// access a context generator aware of the startElementTracking "components" configuration +const [componentGenerator, componentGeneratorWithDetail] = getComponentListGenerator(); + +// this will attach the component_parents entity to events generated by these plugins that show the component hierarchy +enableLinkClickTracking({ options: { ... }, psuedoClicks: true, context: [componentGenerator] }); +enableFormTracking({ context: [componentGenerator] }); + +// componentGeneratorWithDetail will also populate element_detail entities for each component, but is not directly compatible with the above plugin APIs +``` + + + +```json reference title="Entity - iglu:com.snowplowanalytics.snowplow/component_parents/jsonschema/1-0-0" referenceLinkText="See schema on GitHub" +https://github.com/snowplow/iglu-central/blob/master/schemas/com.snowplowanalytics.snowplow/component_parents/jsonschema/1-0-0 +``` + +#### Element contents + +The `element_content` schema gets attached when you use the [`contents` setting](#element-data). + +There can be many instances of this entity in individual events, and the list of them are a flattened tree representation of the nested configuration provided. +Each instance contains references to the parent `element_content` or `element` entity instance that contains it in the `parent_name` and `parent_index` properties (via `element_name` and `element_index`, respectively). +Nested `details` configurations are also used to populate the `attributes` for each instance. + +```json reference title="Entity - iglu:com.snowplowanalytics.snowplow/element_content/jsonschema/1-0-0" referenceLinkText="See schema on GitHub" +https://github.com/snowplow/iglu-central/blob/master/schemas/com.snowplowanalytics.snowplow/element_content/jsonschema/1-0-0 +``` + +#### Custom context + +You can attach custom entities to the events generated by the plugin. + +- You can include `context` alongside `elements` when calling `startElementTracking`. You can pass an array of static entities _or_ a callback function that returns such an array. The function receives the element that the event is relevant to, and the matching rule that defined the event should fire. +- Individual rules may also contain specific `context` in the same format as in the preceding. diff --git a/src/remark/abbreviations.js b/src/remark/abbreviations.js index 14d4b9b812..2af1c27d16 100644 --- a/src/remark/abbreviations.js +++ b/src/remark/abbreviations.js @@ -7,6 +7,7 @@ const plugin = () => { AWS: 'Amazon Web Services', BDP: 'Behavioral Data Platform', CDI: 'Customer Data Infrastructure', + CDN: 'Content Delivery Network', CDP: 'Customer Data Platform', CLI: 'Command Line Interface', EC2: 'Amazon Elastic Compute Cloud', From e69f4976edf584c7a9ae7c48835ef49b74422110 Mon Sep 17 00:00:00 2001 From: John Reid Date: Wed, 30 Apr 2025 16:08:38 +0100 Subject: [PATCH 07/18] Add Bigquery support to the Data Quality Dashboard (#1228) --- .../failed-events/monitoring-failed-events/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/data-product-studio/data-quality/failed-events/monitoring-failed-events/index.md b/docs/data-product-studio/data-quality/failed-events/monitoring-failed-events/index.md index 5bc51bc2ec..a12951f345 100644 --- a/docs/data-product-studio/data-quality/failed-events/monitoring-failed-events/index.md +++ b/docs/data-product-studio/data-quality/failed-events/monitoring-failed-events/index.md @@ -49,7 +49,7 @@ The detailed view shows the error message as well as other useful metadata (when The data quality dashboard allows you to see failed events directly from your warehouse. Your browser connects directly to an API running within your infrastructure, such that no failed event information flows through Snowplow. The discussion of architecture is important as it highlights the trade-offs between the two ways of monitoring failed events. The aforementioned API is a simple proxy that connects to your warehouse and serves the failed events to your browser. The connection is fully secure, using an encrypted channel (HTTPS) and authenticating/authorizing via the same mechanism used by Console. -In order for you to be able to deploy and use the data quality dashboard, you need to sink failed events to the warehouse via a [Failed Events Loader](/docs/data-product-studio/data-quality/failed-events/exploring-failed-events/warehouse-lake/#setup). The data quality dashboard only supports Snowflake. +In order for you to be able to deploy and use the data quality dashboard, you need to sink failed events to the warehouse via a [Failed Events Loader](/docs/data-product-studio/data-quality/failed-events/exploring-failed-events/warehouse-lake/#setup). The data quality dashboard currently supports Snowflake and Bigquery connections. ![](images/dqd-architecture.png) From 3ec7a1d84bab07c1435ba8fada04410d054899fc Mon Sep 17 00:00:00 2001 From: Peter Perlepes Date: Thu, 1 May 2025 13:43:18 +0300 Subject: [PATCH 08/18] Rename ID service to Cookie Extension service (#1219) * Rename ID service to Cookie Extension service * Rename "ID Service" to "Cookie Extension Service" in GTM (#1226) --------- Co-authored-by: Greg Leonard <45019882+greg-el@users.noreply.github.com> --- docs/get-started/feature-comparison/index.md | 2 +- .../tracking/cookies-and-ad-blockers/index.md | 6 +-- docs/sources/first-party-tracking/index.md | 2 +- .../settings-template/index.md | 4 +- .../web-tracker/browsers/index.md | 54 ++++++++++--------- .../user-and-session-identification/index.md | 2 +- 6 files changed, 37 insertions(+), 33 deletions(-) diff --git a/docs/get-started/feature-comparison/index.md b/docs/get-started/feature-comparison/index.md index 5330681fc4..9aa9d05d99 100644 --- a/docs/get-started/feature-comparison/index.md +++ b/docs/get-started/feature-comparison/index.md @@ -12,7 +12,7 @@ Here is a detailed list of product features, including which are available as pa | [35+ trackers and webhooks](/docs/sources/index.md) | ✅ | ✅ | | First party tracking | ✅ | ✅ | | [Anonymous data collection](/docs/resources/recipes-tutorials/recipe-anonymous-tracking/index.md) | ✅ | ✅ | -| [ID service](/docs/sources/trackers/javascript-trackers/web-tracker/browsers/index.md#what-is-an-id-service) | ✅ | ✅ | +| [Cookie Extension service](/docs/sources/trackers/javascript-trackers/web-tracker/browsers/index.md#what-is-a-cookie-extension-service) | ✅ | ✅ | | High availability and auto-scaling | ✅ | ❌ | | [Enrichments](/docs/pipeline/enrichments/available-enrichments/index.md) | ✅ | ✅ | | [Failed events](/docs/fundamentals/failed-events/index.md) | ✅ | ✅ | diff --git a/docs/get-started/tracking/cookies-and-ad-blockers/index.md b/docs/get-started/tracking/cookies-and-ad-blockers/index.md index 67378e4fc7..8cd4b75fd2 100644 --- a/docs/get-started/tracking/cookies-and-ad-blockers/index.md +++ b/docs/get-started/tracking/cookies-and-ad-blockers/index.md @@ -21,14 +21,14 @@ Although first-party cookies don't allow sharing user and session identifiers ac Using Snowplow, you can set up your collector to be on the same domain as the website, which is the requirement for the use of first-party cookies. To learn how to set up the collector domain, [visit this page](/docs/sources/first-party-tracking/index.md). -**Strategy 2: Snowplow ID service for Safari ITP** +**Strategy 2: Snowplow Cookie Extension service for Safari ITP** As of Safari 16.4 released in April 2023, Safari sets the [lifetime of server-set cookies](https://webkit.org/tracking-prevention/#cname-and-third-party-ip-address-cloaking-defense) to a maximum of 7 days under some circumstances even if they are first-party cookies. This greatly limits the effectiveness of tracking a customer journey where users are not regularly returning to your website. In particular, it affects the `network_userid` identifier in Snowplow events. -Snowplow provides the ID service solution that fully mitigates the impact of this change. -Visit the [documentation for the ID service](/docs/sources/trackers/javascript-trackers/web-tracker/browsers/index.md#itp-mitigation) to learn more. +Snowplow provides the Cookie Extension service solution that fully mitigates the impact of this change. +Visit the [documentation for the Cookie Extension service](/docs/sources/trackers/javascript-trackers/web-tracker/browsers/index.md#itp-mitigation) to learn more. ## 2. Mitigating the impact of ad-blockers diff --git a/docs/sources/first-party-tracking/index.md b/docs/sources/first-party-tracking/index.md index e9b7e3f1b7..5c919191ec 100644 --- a/docs/sources/first-party-tracking/index.md +++ b/docs/sources/first-party-tracking/index.md @@ -16,7 +16,7 @@ When your collector domain (e.g. `collector.snwplow.net`) does not match your pr With first-party tracking, you can configure a custom collector domain (e.g. `c.flowershop.ai`) to match your primary domain (e.g. `flowershop.ai`), sidestepping these limitations. -Note that in light of the [latest ITP restrictions](https://webkit.org/tracking-prevention/#cname-and-third-party-ip-address-cloaking-defense), you will also need to [use an ID service](/docs/sources/trackers/javascript-trackers/web-tracker/browsers/index.md#itp-mitigation) to fully persist the cookies. +Note that in light of the [latest ITP restrictions](https://webkit.org/tracking-prevention/#cname-and-third-party-ip-address-cloaking-defense), you will also need to [use a Cookie Extension service](/docs/sources/trackers/javascript-trackers/web-tracker/browsers/index.md#itp-mitigation) to fully persist the cookies. :::info diff --git a/docs/sources/trackers/google-tag-manager/settings-template/index.md b/docs/sources/trackers/google-tag-manager/settings-template/index.md index 4e85ab77d7..8b13d7757a 100644 --- a/docs/sources/trackers/google-tag-manager/settings-template/index.md +++ b/docs/sources/trackers/google-tag-manager/settings-template/index.md @@ -86,9 +86,9 @@ This setting disables client-side user identifiers but tracks session informatio See [here](/docs/resources/recipes-tutorials/recipe-anonymous-tracking/index.md) for more information on anonymous tracking. -#### ID Service +#### Cookie Extension Service -This allows you to set the endpoint for the [ID service](/docs/sources/trackers/javascript-trackers/web-tracker/browsers/#what-is-an-id-service-). +This allows you to set the endpoint for the [Cookie Extension Service](/docs/sources/trackers/javascript-trackers/web-tracker/browsers/index.md#what-is-a-cookie-extension-service-). ## Cookie Settings diff --git a/docs/sources/trackers/javascript-trackers/web-tracker/browsers/index.md b/docs/sources/trackers/javascript-trackers/web-tracker/browsers/index.md index dc4794b966..552f2de428 100644 --- a/docs/sources/trackers/javascript-trackers/web-tracker/browsers/index.md +++ b/docs/sources/trackers/javascript-trackers/web-tracker/browsers/index.md @@ -53,15 +53,15 @@ As of Safari 16.4 released in April 2023, Safari sets the [lifetime of server-se This greatly limits the effectiveness of tracking a customer journey where users are not regularly returning to your website. Without a strong understanding of your customers, downstream use cases including marketing attribution, product analytics and personalized recommendations are difficult to achieve. -**The ID service can help enhance the confidence in persistent browser identifiers that Snowplow tracking provides and specifically for this guide, the `network_userid`.** +**The Cookie Extension service ,previously ID service, can help enhance the confidence in persistent browser identifiers that Snowplow tracking provides and specifically for this guide, the `network_userid`.** -### What is an ID service ? +### What is a Cookie Extension service ? -An _ID service_, as we chose to call it, is a process that allows for generating a unique browser identifier and enhancing the Snowplow tracking capabilities in environments where the Intelligent Tracking Prevention (ITP) feature is enabled such as on iOS browsers (Safari, Chrome, Firefox) and desktop Safari. +An _Cookie Extension service_, as we chose to call it, is a process that allows for generating a unique browser identifier and enhancing the Snowplow tracking capabilities in environments where the Intelligent Tracking Prevention (ITP) feature is enabled such as on iOS browsers (Safari, Chrome, Firefox) and desktop Safari. -### Developing and deploying an ID service +### Developing and deploying a Cookie Extension service -An ID service is code that needs to be deployed on and executed from the same IP space that serves the main web document of your application. This is probably the web application system or the CDN in front of the application. +An Cookie Extension service is code that needs to be deployed on and executed from the same IP space that serves the main web document of your application. This is probably the web application system or the CDN in front of the application. This code has minimal functionality and based on our experience can either be: @@ -70,50 +70,54 @@ This code has minimal functionality and based on our experience can either be: - **A custom middleware based on the customer’s framework**. E.g. ExpressJS middleware, Next.js middleware, Play custom action etc. which can run on every document request. - **A low-footprint application with a single endpoint**. A Go web server or something along these lines. -#### Developing the ID service code +#### Developing the Cookie Extension service code The responsibilities of this service are: 1. Create a unique identifier (UUID v4) for this browser, set it in a cookie and return it in a `Set-Cookie` response header on a domain accessible by the service at all times. 2. Increase the expiry for the cookie used as the network_userid identifier (by default, a cookie named sp, configured via collector.config.name) which should have the same value as the first cookie. -_The new unique identifier cookie for sake of simplicity in this document will have the name `spIdService`._ +_The new unique identifier cookie for sake of simplicity in this document will have the name `spCookieExtensionService`._ -#### ID service business logic +#### Cookie Extension service business logic -The ID service code should include the following logic: +The Cookie Extension service code should include the following logic: -- If the ID service new identifier cookie already exists on the request, then it should re-set the cookies with the same values and updated expiration for both the `spIdService` and `sp` cookies. -- If `spIdService` does not exist, but the collector's `sp` cookie does exist, then the it should set `spIdService value = sp cookie value`. _This will make sure we keep old identifiers in place and not lose any data._ -- If `spIdService` and `sp` are both missing, then it generates a new ID in the `spIdService` and `sp` cookies with the same unique identifier generation algorithm with the Snowplow pipeline, currently UUID v4. +- If the Cookie Extension service new identifier cookie already exists on the request, then it should re-set the cookies with the same values and updated expiration for both the `spCookieExtensionService` and `sp` cookies. +- If `spCookieExtensionService` does not exist, but the collector's `sp` cookie does exist, then the it should set `spCookieExtensionService value = sp cookie value`. _This will make sure we keep old identifiers in place and not lose any data._ +- If `spCookieExtensionService` and `sp` are both missing, then it generates a new ID in the `spCookieExtensionService` and `sp` cookies with the same unique identifier generation algorithm with the Snowplow pipeline, currently UUID v4. - The HTTP response should have a 200 OK status code but any additional payload is not necessary. ### Code examples -Below we showcase a couple of code samples for ID service API endpoints: +Below we showcase a couple of code samples for Cookie Extension service API endpoints: - + ```ts reference -https://github.com/snowplow-industry-solutions/id-service-examples/blob/main/examples/typescript/Next.js/api-route.ts +https://github.com/snowplow-industry-solutions/cookie-extension-service-examples/blob/main/examples/typescript/Next.js/api-route.ts ``` ```php reference -https://github.com/snowplow-industry-solutions/id-service-examples/blob/main/examples/php/wordpress/api-route.php +https://github.com/snowplow-industry-solutions/cookie-extension-service-examples/blob/main/examples/php/wordpress/api-route.php ``` -### Using the ID service on the Snowplow browser tracker +### Using the Cookie Extension service on the Snowplow browser tracker -When the ID service has been deployed on a system with the same resolved IP as the main document, the tracker can then be configured to orchestrate the required ID service API calls. +:::note +Before version 4.5.0 of the tracker this attribute was available as `idService`. +::: + +When the Cookie Extension service has been deployed on a system with the same resolved IP as the main document, the tracker can then be configured to orchestrate the required Cookie Extension service API calls. -This process is opt-in by using the `idService` option during tracker initialization: +This process is opt-in by using the `cookieExtensionService` option during tracker initialization: @@ -121,7 +125,7 @@ This process is opt-in by using the `idService` option during tracker initializa ```tsx window.snowplow("newTracker", "sp", "{{collector_url_here}}", { /* ...Rest of the tracker options */ - idService: "/id-service-endpoint" + cookieExtensionService: "/cookie-extension-service-endpoint" }); ``` @@ -130,7 +134,7 @@ window.snowplow("newTracker", "sp", "{{collector_url_here}}", { ```tsx newTracker('sp1', 'c.customer.com', { - idService: "/id-service-endpoint", + cookieExtensionService: "/cookie-extension-service-endpoint", /* ...Rest of the tracker options */ }); ``` @@ -143,15 +147,15 @@ When the tracker detects this option it will send an HTTP request during initial ```mermaid sequenceDiagram autonumber - participant Service as ID Service Endpoint + participant Service as Cookie Extension service Endpoint participant Tracking as Snowplow Tracking Code participant Collector as Snowplow Collector Tracking->>Service: Request an ID - note over Service: ID Service code + note over Service: Cookie Extension service code Service->>Tracking: OK response - note over Tracking,Service: Set-Cookie: sp=...
Set-Cookie: spIdService=... + note over Tracking,Service: Set-Cookie: sp=...
Set-Cookie: spCookieExtensionService=... loop Normal flow Tracking->>Collector: Event Tracking - note over Tracking,Collector: Cookie: sp=...#59; spIdService=... + note over Tracking,Collector: Cookie: sp=...#59; spCookieExtensionService=... end ``` diff --git a/docs/sources/trackers/snowplow-tracker-protocol/ootb-data/user-and-session-identification/index.md b/docs/sources/trackers/snowplow-tracker-protocol/ootb-data/user-and-session-identification/index.md index 8924a0f16b..05f1426786 100644 --- a/docs/sources/trackers/snowplow-tracker-protocol/ootb-data/user-and-session-identification/index.md +++ b/docs/sources/trackers/snowplow-tracker-protocol/ootb-data/user-and-session-identification/index.md @@ -48,7 +48,7 @@ The Snowplow Collector generates a user identifier that is stored in cookies for The identifier is available both in Web and mobile apps. However, in Android apps, it is stored in memory so it is reset after the app restarts. -In most scenarios, this identifier may have a longer lifetime than the tracker generated identifier. However, browsers can restrict it's lifetime for different reasons, such as when the Snowplow Collector is on a third-party domain from the website (not recommended), or due to the ITP restrictions in Safari (Snowplow provides a solution to mitigate this problem – [the ID service](/docs/sources/trackers/javascript-trackers/web-tracker/browsers/index.md#itp-mitigation)). +In most scenarios, this identifier may have a longer lifetime than the tracker generated identifier. However, browsers can restrict it's lifetime for different reasons, such as when the Snowplow Collector is on a third-party domain from the website (not recommended), or due to the ITP restrictions in Safari (Snowplow provides a solution to mitigate this problem – [the Cookie Extension service](/docs/sources/trackers/javascript-trackers/web-tracker/browsers/index.md#itp-mitigation)). :::info `network_userid` is captured via a cookie set by the Snowplow Collector. It can be overriden by setting `tnuid` on a Tracker request payload but is typically expected to be populated by the Collector cookies. From a0703a9e9b6bce4ee6998c7ac0e4b56cdda28560 Mon Sep 17 00:00:00 2001 From: Oguzhan Unlu Date: Tue, 6 May 2025 14:40:47 +0300 Subject: [PATCH 09/18] Add lake loader 0.6.3 & Mini 0.23.0 (#1231) --- src/componentVersions.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/componentVersions.js b/src/componentVersions.js index b0e8c40fb3..c078be1562 100644 --- a/src/componentVersions.js +++ b/src/componentVersions.js @@ -38,7 +38,7 @@ export const versions = { rdbLoader: '6.1.3', s3Loader: '2.2.9', s3Loader22x: '2.2.9', - lakeLoader: '0.6.2', + lakeLoader: '0.6.3', snowflakeStreamingLoader: '0.4.1', // Data Modelling @@ -78,5 +78,5 @@ export const versions = { // Testing & debugging snowplowMicro: '2.2.0', - snowplowMini: '0.22.0', + snowplowMini: '0.23.0', } From de429bd977ea6b2de479784d1e73c0c6da66899e Mon Sep 17 00:00:00 2001 From: Miranda Wilson Date: Wed, 7 May 2025 19:03:54 +0100 Subject: [PATCH 10/18] Add Product Fruits (#1181) * Remove the old feedback widget * Add product fruits package * Add netlify function * Add Product Fruits to Root * Update yarn.lock --- .gitignore | 3 + netlify/functions/product_fruits_key.js | 5 + package.json | 1 + src/css/custom.css | 15 +- src/theme/DocItem/Footer/index.js | 136 - src/theme/DocItem/Footer/styles.module.css | 36 - src/theme/Root.js | 68 +- yarn.lock | 6562 ++++++++++---------- 8 files changed, 3267 insertions(+), 3559 deletions(-) create mode 100644 netlify/functions/product_fruits_key.js delete mode 100644 src/theme/DocItem/Footer/index.js delete mode 100644 src/theme/DocItem/Footer/styles.module.css diff --git a/.gitignore b/.gitignore index 408f2a80d7..6e7b4d92b9 100644 --- a/.gitignore +++ b/.gitignore @@ -28,3 +28,6 @@ yarn-error.log* # Python __pycache__ manifests + +# Local Netlify folder +.netlify diff --git a/netlify/functions/product_fruits_key.js b/netlify/functions/product_fruits_key.js new file mode 100644 index 0000000000..5ff019ac95 --- /dev/null +++ b/netlify/functions/product_fruits_key.js @@ -0,0 +1,5 @@ +exports.handler = async () => ({ + statusCode: 200, + headers: { 'Content-Type': 'text/plain' }, + body: process.env.PRODUCT_FRUITS || '', +}) diff --git a/package.json b/package.json index f8d720e510..1af5519ca0 100644 --- a/package.json +++ b/package.json @@ -66,6 +66,7 @@ "react-dom": "^17.0.2", "react-markdown": "^8.0.5", "react-player": "^2.10.1", + "react-product-fruits": "^2.2.61", "rehype-katex": "5", "remark-gfm": "^3.0.1", "remark-math": "3", diff --git a/src/css/custom.css b/src/css/custom.css index 0be2f6c586..5f236e34f0 100644 --- a/src/css/custom.css +++ b/src/css/custom.css @@ -589,20 +589,7 @@ Controls the display of navbar/sidebar items margin-top: 2rem; } -.tutorial-doc-page .feedback-prompt { - margin-top: 3rem; -} - -.tutorial-doc-page .theme-doc-footer { - display: none; - margin-top: 0; -} - -.tutorial-doc-page .feedback-prompt { - margin-top: 2rem; -} - -/* Hide the feedback and last updated times on the tutorial home page */ +/* Hide the last updated times on the tutorial home page */ .tutorial-home-page article > footer { display: none; } diff --git a/src/theme/DocItem/Footer/index.js b/src/theme/DocItem/Footer/index.js deleted file mode 100644 index e3abccdc45..0000000000 --- a/src/theme/DocItem/Footer/index.js +++ /dev/null @@ -1,136 +0,0 @@ -import React, { useState, useRef } from 'react' -import clsx from 'clsx' -import Footer from '@theme-original/DocItem/Footer' -import styles from './styles.module.css' -import { trackStructEvent } from '@snowplow/browser-tracker' -import { useLocation } from '@docusaurus/router' -import { useDoc } from '@docusaurus/theme-common/internal' - -function CommentBox({ handleSubmit, feedbackTextRef }) { - const placeholder = 'How can we improve it?' - - return ( -
-