Issue with Heatmap Tracking on Hero Component Buttons

Subject: Issue with Heatmap Tracking on Hero Component Buttons

Message:

Hello,

I have a quick question regarding heatmap tracking on our homepage. In our hero section, we have a child component that contains buttons.

We’ve noticed that some clicks are being registered on an image within the hero, but no clicks are being tracked on the buttons themselves. This seems inaccurate, as we expect interactions on those buttons.

Could you please advise on the best way to debug this issue and help us understand why the button clicks might not be tracked properly?

Thank you!

Best regards,

Hi Tony​,

Thank you for contacting us. I will check this internally and get back to you as soon as I have an update.

We appreciate your patience and understanding.

Thanks,

Hi Tony,

Could you let me know which SDK version you’re currently using (Check in package.json file)? Please try checking with the latest version of the SDK. I tested it on my end using the most recent version, and everything seems to be working fine.

If possible, could you share a GitHub repository with minimal code or details so I can try to reproduce the issue on my side?

Thanks,

Our SDK is @builder.io/react": "^8.0.2",

Our hero component is like the following under component > heroNew folder:

import type { StyledHeroNewProps } from '@/components/HeroNew/consts';
import Heading from '@/molecules/Heading';
import { StyledHeadingWithEyebrow } from '@/molecules/HeadingWithEyebrow/styled';
import Text from '@/molecules/Text';
import { mediaQuery } from '@/styles/breakpoints/ts/breakpoints';
import { colorToken } from '@/styles/colors/ts/colorToken';
import { effects } from '@/styles/effects/ts/effects';
import { borderRadii } from '@/styles/radii/ts/borderRadii';
import { spacings } from '@/styles/spacings/ts/spacings';
import type { StyledType } from '@/utils/types/styledTypeConverter';
import styled, { css } from 'styled-components';
import { headingLetterSpacings, headingLineHeights, headingSizes } from '@/styles/typography/ts/typography';
import HeadingWithEyebrow from '@/molecules/HeadingWithEyebrow';
import { DSNextImage } from '@/components/DSNextImage';

export const StyledHeroNew = styled.section<StyledType<StyledHeroNewProps & { hasOverflowingMedia: boolean }>>`
  display: flex;
  flex-direction: column;
  gap: ${spacings.spacing5XL};
  align-items: center;
  justify-content: center;
  padding: calc(${spacings.spacing5XL} + 2rem) ${spacings.spacingXL}
    calc(${({ $hasOverflowingMedia }) => ($hasOverflowingMedia ? '3' : '1')} * ${spacings.spacing5XL}); // 2rem because of crappy layout
  margin: 0 10px;
  ${({ $marginNav }) => $marginNav && `margin-top: ${spacings.spacing4XL};`}
  border-radius: ${borderRadii.radiusLG};
  position: relative;

  ${({ $backgroundColor }) => {
    if (!$backgroundColor) return;

    if ($backgroundColor in effects) {
      return `background: ${effects[$backgroundColor as keyof typeof effects]}`;
    }

    return `background-color: ${colorToken[$backgroundColor as keyof typeof colorToken]}`;
  }};

  ${mediaQuery.small} {
    margin: 0 ${spacings.spacingXL};
    ${({ $marginNav }) => $marginNav && `margin-top: ${spacings.spacing7XL};`}
    gap: ${spacings.spacing6XL};
    padding: calc(${spacings.spacing6XL} + 2.25rem) ${spacings.spacingXL}
      calc(${({ $hasOverflowingMedia }) => ($hasOverflowingMedia ? '3' : '1')} * ${spacings.spacing6XL}); // 2.25rem because of crappy layout (nav overlay)
  }

  ${mediaQuery.medium} {
    gap: ${({ $variant }) => ($variant === '2column' ? spacings.spacing8XL : spacings.spacing7XL)};
    padding: calc(${spacings.spacing7XL} + 2.25rem) ${spacings.spacingXL}
      calc(${({ $hasOverflowingMedia }) => ($hasOverflowingMedia ? '3' : '1')} * ${spacings.spacing7XL}); // 2.25rem because of crappy layout (nav overlay)
    ${({ $variant }) => $variant === '2column' && 'flex-direction: row;'}
  }
`;

export const StyledDSBackgroundImage = styled(DSNextImage)`
  position: absolute;
  z-index: 1;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  max-width: 100%;
  object-fit: contain;
  display: none;

  ${mediaQuery.medium} {
    display: block;
  }
`;

export const StyledDSBottomImage = styled(DSNextImage)`
  display: block;
  margin: ${spacings.spacingLG} auto 0;
  max-width: 100%;
  object-fit: contain;

  ${mediaQuery.medium} {
    display: none;
  }
`;

export const ContentWrapper = styled.div<{ $variant?: '1column' | '2column' }>`
  width: 100%;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: ${spacings.spacing5XL};

  ${mediaQuery.small} {
    gap: ${spacings.spacing6XL};
  }

  ${mediaQuery.medium} {
    gap: ${spacings.spacing7XL};

    ${({ $variant }) =>
      $variant === '2column'
        ? css`
            max-width: 30rem; // 480px
            text-align: left;
            align-items: flex-start;
          `
        : css`
            max-width: 48.75rem; // 780px
          `};
  }
`;

export const ContentInner = styled.div<{ $variant?: '1column' | '2column' }>`
  width: 100%;
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  z-index: 2;

  ${mediaQuery.small} {
    max-width: 36.25rem; // 580px
  }

  ${mediaQuery.medium} {
    max-width: unset;
    ${({ $variant }) =>
      $variant === '2column' &&
      css`
        text-align: left;
        align-items: flex-start;

        // force the heading & eyebrow to the left in 2column desktop
        ${StyledHeadingWithEyebrow} {
          align-items: flex-start;
          text-align: left;
        }

        h1 {
          text-align: left;
        }

        div > div > div > button {
          width: 100%;
        }
      `};
  }
`;

export const StyledHeading = styled(HeadingWithEyebrow)`
  > *:last-child {
    font-size: ${headingSizes.heading1};
    line-height: ${headingLineHeights.heading1};
    letter-spacing: ${headingLetterSpacings.heading1};

    ${mediaQuery.medium} {
      ${({ type }) =>
        type === 'heading2' &&
        css`
          font-size: ${headingSizes.heading2};
          line-height: ${headingLineHeights.heading2};
          letter-spacing: ${headingLetterSpacings.heading2};
        `};
    }
  }
`;

export const Subtitle = styled(Heading)`
  font-size: ${headingSizes.heading3};
  line-height: ${headingLineHeights.heading3};
  letter-spacing: ${headingLetterSpacings.heading3};
  margin-top: ${spacings.spacingXS};

  ${mediaQuery.small} {
    margin-top: ${spacings.spacingSM};
  }

  ${mediaQuery.medium} {
    ${({ type }) =>
      type === 'heading4' &&
      css`
        font-size: ${headingSizes.heading4};
        line-height: ${headingLineHeights.heading4};
        letter-spacing: ${headingLetterSpacings.heading4};
      `};
  }
`;

export const Message = styled(Text)`
  margin-top: ${spacings.spacingLG};

  ${mediaQuery.small} {
    margin-top: ${spacings.spacing2XL};
  }
`;

export const ButtonWrapper = styled.div`
  width: 100%;
  display: flex;
  flex-direction: column;
  gap: ${spacings.spacingMD};
  align-items: stretch;
  margin-top: ${spacings.spacing2XL};

  ${mediaQuery.small} {
    width: auto;
    flex-direction: row;
    justify-content: center;
    gap: ${spacings.spacingLG};
    margin-top: ${spacings.spacing3XL};
  }
`;

export const SecondColumn = styled.div<{ $variant?: '1column' | '2column' }>`
  width: 100%;

  ${({ $variant }) =>
    $variant === '1column' &&
    css`
      display: flex;
      justify-content: center;
    `};

  ${mediaQuery.medium} {
    ${({ $variant }) =>
      $variant === '2column' &&
      css`
        width: calc(50% - ${spacings.spacingSM});
        max-width: 36.25rem;
        flex-shrink: 0;
      `};
  }
`;

export const MediaWrapper = styled.div`
  width: 100%;
  max-width: 73.75rem; // 1180px
  border-radius: ${borderRadii.radiusSM};
  position: relative;
`;

export const FormWrapper = styled.div`
  max-width: 73.75rem; // 1180px
`;

export const OverflowingMedia = styled.div`
  margin: calc(-2 * ${spacings.spacing5XL}) auto ${spacings.spacing5XL};
  width: 100%;
  max-width: calc(100% - (2 * ${spacings.spacingXL}));

  ${mediaQuery.small} {
    margin: calc(-2 * ${spacings.spacing6XL}) auto ${spacings.spacing6XL};
    max-width: calc(100% - (4 * ${spacings.spacingXL}));
  }

  ${mediaQuery.medium} {
    margin: calc(-2 * ${spacings.spacing7XL}) auto ${spacings.spacing7XL};
    max-width: min(calc(100% - (4 * ${spacings.spacingXL})), 73.75rem);
  }
`;

and our builder config file is separated into two in hero-presenter folder:

index.tsx

import type { BackgroundImage, HeroNewProps } from '@/components/HeroNew/consts';

import HeroNew from '@/components/HeroNew';
import type { HeadingWithEyebrowProps } from '@/molecules/HeadingWithEyebrow/consts';
import { type BasePresenterProps, builderPresenter } from '@/utils/builder/builderPresenter';
import injectSizesIntoImages from '@/utils/builder/injectSizesIntoImages';
import { Builder, BuilderBlockComponent, BuilderBlocks } from '@builder.io/react';
import type { BuilderElement } from '@builder.io/sdk';
import styled from 'styled-components';

// Properties for the presenter (must match Builder inputs)
export type HeroPresenterProps = BasePresenterProps & {
  variant: '1column' | '2column';
  eyebrow?: HeadingWithEyebrowProps['eyebrow'];
  title: string;
  subtitle?: string;
  message?: string;
  media?: BuilderElement[];
  enableForm?: boolean;
  enableImage?: boolean;
  form?: BuilderElement[];
  backgroundColor?: 'bgSecondary' | '';
  darkMode?: false;
  backgroundImage?: BackgroundImage;
  marginNav?: boolean;
};

const StyledBuilderBlocks = styled(BuilderBlocks)`
  display: flex;
  flex-direction: column;
  align-items: center !important;

  .builder-block {
    display: flex;
    width: 100%;
    justify-content: center;
  }
`;

// Props transformation
export const transformHeadingProps = ({
  variant,
  eyebrow,
  title,
  subtitle,
  message,
  media,
  enableForm,
  form,
  enableImage,
  backgroundColor,
  darkMode,
  backgroundImage,
  builderBlock,
  marginNav,
}: HeroPresenterProps): HeroNewProps => ({
  variant,
  title: {
    eyebrow,
    children: title,
  },
  subtitle,
  message,
  buttons: builderBlock.children?.map(child => <BuilderBlockComponent block={child} key={child.id} />),
  media:
    enableImage && (Builder.isEditing || (media && media?.length > 0)) ? (
      <StyledBuilderBlocks
        blocks={injectSizesIntoImages(media, variant === '2column' ? '(min-width: 1024px) 50vw, 100vw' : '100vw')}
        parentElementId={builderBlock.id}
        dataPath="component.options.media"
        child
      />
    ) : undefined,
  form: enableForm ? (
    <BuilderBlocks blocks={form} parentElementId={builderBlock.id} dataPath="component.options.form" child />
  ) : undefined,
  backgroundColor: backgroundColor || undefined,
  darkMode: darkMode,
  backgroundImage: backgroundImage || undefined,
  marginNav,
});

// Export the presenter
export default builderPresenter(HeroNew, transformHeadingProps);

and hero-builder.tsx

import { EyebrowInputs } from '@/builder-presenters/Heading/heading.builder';
import type { Component, Input } from '@builder.io/sdk';
import { getBuilderBackgroundColors } from '@/utils/builder/builderConfigHelpers';

export const HeroInputs: Input[] = [
  {
    name: 'variant',
    type: 'text',
    enum: ['1column', '2column'],
    defaultValue: '1column',
  },
  {
    name: 'eyebrow',
    type: 'object',
    subFields: EyebrowInputs,
  },
  {
    name: 'title',
    type: 'string',
    required: true,
    defaultValue: 'Hero Title',
  },
  {
    name: 'subtitle',
    type: 'string',
    defaultValue: 'Hero Subtitle',
  },
  {
    name: 'message',
    type: 'string',
    defaultValue: 'Hero Message',
  },
  {
    name: 'enableImage',
    friendlyName: 'Enable Media',
    helperText: 'Enable overflown image/video/embed in the hero section',
    type: 'boolean',
    defaultValue: false,
  },
  {
    name: 'media',
    type: 'uiBlocks',
    hideFromUI: true,
    defaultValue: [
      {
        '@type': '@builder.io/sdk:Element',
        component: {
          name: 'dsImage',
          options: {
            image: 'https://picsum.photos/1200/800',
            width: 1200,
            height: 800,
          },
        },
      },
    ],
  },
  {
    name: 'enableForm',
    friendlyName: 'Enable Form',
    helperText: 'Enable form in the hero section.',
    type: 'boolean',
  },
  {
    name: 'form',
    type: 'uiBlocks',
    hideFromUI: true,
    defaultValue: [
      {
        '@type': '@builder.io/sdk:Element',
        component: {
          name: 'Form',
          options: {
            formId: '1071',
            showHeading: true,
            businessEmailValidation: false,
            redirectUrl: '',
            paddingDisabled: false,
            submittedBox: {
              hasCta: false,
              heading: 'Heading',
              text: 'Text',
            },
            failedBox: {
              hasCta: false,
              heading: 'Heading',
              text: 'Text',
            },
          },
        },
      },
    ],
  },
  {
    name: 'backgroundColor',
    type: 'string',
    enum: [
      ...getBuilderBackgroundColors(),
      {
        label: 'Blue Gradient',
        value: 'gradientBlue',
      },
    ],
    defaultValue: 'bgSecondary',
  },
  {
    name: 'darkMode',
    type: 'boolean',
    friendlyName: 'Enable Dark Mode',
    defaultValue: false,
  },
  {
    name: 'backgroundImage',
    friendlyName: 'Background Image',
    type: 'object',
    subFields: [
      {
        name: 'src',
        type: 'file',
        allowedFileTypes: ['jpeg', 'png', 'jpg', 'svg', 'gif'],
      },
      {
        name: 'alt',
        type: 'string',
      },
    ],
  },
  {
    name: 'marginNav',
    type: 'boolean',
    friendlyName: 'Margin for navigation (NOT RECOMMENDED)',
    defaultValue: false,
  },
];

export const HeroComponent: Component = {
  name: 'Hero',
  image:
    'https://cdn.builder.io/api/v1/image/assets%2F1d8ecee591ac4358befb8fe998100548%2F2ae2a73e186f41f090aa0ed18448c6b6',
  canHaveChildren: true,
  noWrap: true,
  inputs: HeroInputs,
  childRequirements: {
    message: 'You can only add Button, Video Button and Marketo Button here',
    query: {
      'component.name': { $in: ['dsButton', 'FormButton', 'videoButton', 'Image', 'dsImage'] },
    },
  },
  defaultChildren: [
    {
      '@type': '@builder.io/sdk:Element',
      component: {
        name: 'dsButton',
        options: {
          children: 'Primary button',
          type: 'primary',
        },
      },
    },
    {
      '@type': '@builder.io/sdk:Element',
      component: {
        name: 'dsButton',
        options: {
          children: 'Secondary button',
          type: 'secondary',
        },
      },
    },
  ],
};

Hi Tony,

The issue is because of the use noWrap: true in the HeroComponent. When using noWrap: true, it is important to pass {...props.attributes} to ensure class names are assigned correctly as in the following example.

import { TextField } from '@material-ui/core'

export const BuilderTextField = props => (
  // Important! Builder.io must add a couple classes 
  // and attributes with props.attributes.
  // If you add your own classes, 
  // do it after ...props.attributes
   
  <TextField 
    variant={props.variant} 
    {...props.attributes} 
    className={`my-class ${props.attributes.className}`}
   />
)

Builder.registerComponent(BuilderTextField, {
  name: 'TextField',
  noWrap: true, // Important!
  inputs: [{ name: 'variant', type: 'string' }]
})

For more information, you can check this post: Registering Custom Components.

Do let me know if you need any further assistance.

Thanks,

Hello Sharma,

I have modified it slightly to our builder config like the following:

// Props transformation
export const transformHeadingProps = ({
  variant,
  eyebrow,
  title,
  subtitle,
  message,
  media,
  enableForm,
  form,
  enableImage,
  backgroundColor,
  darkMode,
  backgroundImage,
  builderBlock,
  marginNav,
  ...props
}: HeroPresenterProps): HeroNewProps => ({
  variant,
  title: {
    eyebrow,
    children: title,
  },
  subtitle,
  message,
  buttons: builderBlock.children?.map(child => <BuilderBlockComponent block={child} key={child.id} />),
  media:
    enableImage && (Builder.isEditing || (media && media?.length > 0)) ? (
      <StyledBuilderBlocks
        blocks={injectSizesIntoImages(media, variant === '2column' ? '(min-width: 1024px) 50vw, 100vw' : '100vw')}
        parentElementId={builderBlock.id}
        dataPath="component.options.media"
        child
      />
    ) : undefined,
  form: enableForm ? (
    <BuilderBlocks blocks={form} parentElementId={builderBlock.id} dataPath="component.options.form" child />
  ) : undefined,
  backgroundColor: backgroundColor || undefined,
  darkMode: darkMode,
  backgroundImage: backgroundImage || undefined,
  marginNav,
  ...props.attributes,
});

But I am not sure how this might resolve the glitch, since we are not modifying any className. Additionally, if this resolves the glitch, would the result reflect immediately on the heatmap with the existing data, or it would take sometimes for heatmap to gather the data from the new user interaction?

Hi Tony,

I can understand that you’re not modifying the class name but it’s important to pass {...props.attributes} when using noWrap: true to ensure class names are applied correctly.

Also, please note that heatmap tracking will not capture past data retroactively. Tracking will begin from the moment the code is fixed and deployed.

Thanks,

If we have {...props} spread operator already in some of our components, wouldn’t that be enough?

Hi Tony,

No, spreading just {...props} is not enough.
You still need to explicitly spread props.attributes , even if you’re already doing {...props}.

Thanks,

1 Like