Skip to content

Commit

Permalink
📈(lld) optimize braze CC analytics (#8361)
Browse files Browse the repository at this point in the history
  • Loading branch information
LucasWerey authored Nov 15, 2024
1 parent 61f8b03 commit 34cf0f9
Show file tree
Hide file tree
Showing 14 changed files with 127 additions and 144 deletions.
5 changes: 5 additions & 0 deletions .changeset/sour-badgers-matter.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"ledger-live-desktop": patch
---

Optimize content card log impression based on 50% of the card seen
1 change: 0 additions & 1 deletion apps/ledger-live-desktop/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,6 @@
"react-dom": "18.3.1",
"react-easy-crop": "4.7.5",
"react-i18next": "13.5.0",
"react-intersection-observer": "8.34.0",
"react-is": "17.0.2",
"react-key-handler": "1.2.0-beta.3",
"react-lottie": "1.2.4",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import React, { useRef, useEffect, useMemo } from "react";
import * as braze from "@braze/web-sdk";
import { useSelector } from "react-redux";
import { trackingEnabledSelector } from "~/renderer/reducers/settings";
import { track } from "~/renderer/analytics/segment";
import { Flex } from "@ledgerhq/react-ui";

interface LogContentCardWrapperProps {
id: string;
children: React.ReactNode;
additionalProps?: object;
}

const PERCENTAGE_OF_CARD_VISIBLE = 0.5;

const LogContentCardWrapper: React.FC<LogContentCardWrapperProps> = ({
id,
children,
additionalProps,
}) => {
const ref = useRef<HTMLDivElement>(null);
const isTrackedUser = useSelector(trackingEnabledSelector);

const currentCard = useMemo(() => {
const cards = braze.getCachedContentCards().cards;
return cards.find(card => card.id === id);
}, [id]);

useEffect(() => {
if (!currentCard || !isTrackedUser) return;

const intersectionObserver = new IntersectionObserver(
([entry]) => {
if (entry.intersectionRatio > PERCENTAGE_OF_CARD_VISIBLE) {
braze.logContentCardImpressions([currentCard]);
track("contentcard_impression", {
id: currentCard.id,
...currentCard.extras,
...additionalProps,
});
}
},
{ threshold: PERCENTAGE_OF_CARD_VISIBLE },
);

const currentRef = ref.current;

if (currentRef) {
intersectionObserver.observe(currentRef);
}

return () => {
if (currentRef) {
intersectionObserver.unobserve(currentRef);
}
};
}, [currentCard, isTrackedUser, additionalProps]);

return (
<Flex width="100%" ref={ref}>
{children}
</Flex>
);
};

export default LogContentCardWrapper;
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import { ActionContentCard, PortfolioContentCard } from "~/types/dynamicContent";
import {
ActionContentCard,
PortfolioContentCard,
NotificationContentCard,
} from "~/types/dynamicContent";

export const setPortfolioCards = (payload: PortfolioContentCard[]) => ({
type: "DYNAMIC_CONTENT_SET_PORTFOLIO_CARDS",
Expand All @@ -10,7 +14,7 @@ export const setActionCards = (payload: ActionContentCard[]) => ({
payload,
});

export const setNotificationsCards = (payload: PortfolioContentCard[]) => ({
export const setNotificationsCards = (payload: NotificationContentCard[]) => ({
type: "DYNAMIC_CONTENT_SET_NOTIFICATIONS_CARDS",
payload,
});
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,6 @@ type SlideRes = {

export const useDefaultSlides = (): {
slides: SlideRes[];
logSlideImpression: (index: number) => void;
dismissCard: (index: number) => void;
} => {
const [cachedContentCards, setCachedContentCards] = useState<braze.Card[]>([]);
Expand All @@ -65,21 +64,6 @@ export const useDefaultSlides = (): {
setCachedContentCards(cards);
}, []);

const logSlideImpression = useCallback(
(index: number) => {
if (portfolioCards && portfolioCards.length > index) {
const slide = portfolioCards[index];
if (slide?.id) {
const currentCard = cachedContentCards.find(card => card.id === slide.id);
if (currentCard) {
isTrackedUser && braze.logContentCardImpressions([currentCard]);
}
}
}
},
[portfolioCards, cachedContentCards, isTrackedUser],
);

const dismissCard = useCallback(
(index: number) => {
if (portfolioCards && portfolioCards.length > index) {
Expand Down Expand Up @@ -129,7 +113,6 @@ export const useDefaultSlides = (): {

return {
slides,
logSlideImpression,
dismissCard,
};
};
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useCallback, useEffect, useMemo, useState } from "react";
import React, { useCallback, useMemo, useState } from "react";
import styled from "styled-components";
import { useTransition, animated } from "react-spring";
import IconArrowRight from "~/renderer/icons/ArrowRight";
Expand All @@ -8,6 +8,7 @@ import TimeBasedProgressBar from "~/renderer/components/Carousel/TimeBasedProgre
import IconCross from "~/renderer/icons/Cross";
import { getTransitions, useDefaultSlides } from "~/renderer/components/Carousel/helpers";
import { track } from "~/renderer/analytics/segment";
import LogContentCardWrapper from "LLD/components/DynamicContent/LogContentCardWrapper";

const CarouselWrapper = styled(Card)`
position: relative;
Expand Down Expand Up @@ -149,28 +150,19 @@ const Carousel = ({
speed?: number;
type?: "slide" | "flip";
}) => {
const { slides, logSlideImpression, dismissCard } = useDefaultSlides();
const { slides, dismissCard } = useDefaultSlides();
const [index, setIndex] = useState(0);
const [paused, setPaused] = useState(false);
const [reverse, setReverse] = useState(false);
const transitions = useTransition(index, p => p, getTransitions(type, reverse));
const [hasLoggedFirstImpression, setHasLoggedFirstImpression] = useState(false);

useEffect(() => {
if (!hasLoggedFirstImpression) {
setHasLoggedFirstImpression(true);
logSlideImpression(0);
}
}, [hasLoggedFirstImpression, logSlideImpression]);

const changeVisibleSlide = useCallback(
(newIndex: number) => {
if (index !== newIndex) {
setIndex(newIndex);
logSlideImpression(newIndex);
}
},
[index, logSlideImpression],
[index],
);

const onChooseSlide = useCallback(
Expand Down Expand Up @@ -232,10 +224,12 @@ const Carousel = ({
<Slides>
{transitions.map(({ item, props, key }) => {
if (!slides?.[item]) return null;
const { Component } = slides[item];
const { Component, id } = slides[item];
return (
<animated.div key={key} style={{ ...props }}>
<Component />
<LogContentCardWrapper id={id}>
<Component />
</LogContentCardWrapper>
</animated.div>
);
})}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import React, { useEffect } from "react";
import React from "react";
import ButtonV3 from "~/renderer/components/ButtonV3";
import { Actions, Body, CardContainer, Header, Description, Title } from "./components";
import { Link } from "@ledgerhq/react-ui";
import { useInView } from "react-intersection-observer";

type Props = {
img?: string;
Expand All @@ -23,19 +22,11 @@ type Props = {
dataTestId?: string;
};
};

onView?: Function;
};

const ActionCard = ({ img, leftContent, title, description, actions, onView }: Props) => {
const { ref, inView } = useInView({ threshold: 0.5, triggerOnce: true });

useEffect(() => {
if (inView) onView?.();
}, [onView, inView]);

const ActionCard = ({ img, leftContent, title, description, actions }: Props) => {
return (
<CardContainer ref={ref}>
<CardContainer>
{(img && <Header src={img} />) || leftContent}
<Body>
<Title>{title}</Title>
Expand Down
4 changes: 2 additions & 2 deletions apps/ledger-live-desktop/src/renderer/components/Page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { useLocation } from "react-router-dom";
import styled, { DefaultTheme, ThemedStyledProps } from "styled-components";
import AngleUp from "~/renderer/icons/AngleUp";
import TopBar from "~/renderer/components/TopBar";
import PortfolioContentCards from "~/renderer/screens/dashboard/ActionContentCards";
import ActionContentCards from "~/renderer/screens/dashboard/ActionContentCards";
import { ABTestingVariants } from "@ledgerhq/types-live";

type Props = {
Expand Down Expand Up @@ -154,7 +154,7 @@ const Page = ({ children }: Props) => {
<AngleUp size={20} />
</ScrollUpButton>
{/* Only on dashboard page */}
{pathname === "/" && <PortfolioContentCards variant={ABTestingVariants.variantB} />}
{pathname === "/" && <ActionContentCards variant={ABTestingVariants.variantB} />}
</PageContainer>
);
};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import React, { useCallback, useRef, useMemo } from "react";
import React, { useCallback, useMemo } from "react";
import styled from "styled-components";
import { Trans } from "react-i18next";
import { InView } from "react-intersection-observer";
import { useDispatch } from "react-redux";
import InfoCircle from "~/renderer/icons/InfoCircle";
import TriangleWarning from "~/renderer/icons/TriangleWarning";
Expand All @@ -16,6 +15,7 @@ import { useNotifications } from "~/renderer/hooks/useNotifications";
import TrackPage from "~/renderer/analytics/TrackPage";
import { urls } from "~/config/urls";
import { useDateFormatted } from "~/renderer/hooks/useDateFormatter";
import LogContentCardWrapper from "LLD/components/DynamicContent/LogContentCardWrapper";

const DateRowContainer = styled.div`
padding: 4px 16px;
Expand Down Expand Up @@ -243,27 +243,7 @@ const Separator = styled.div`
`;

export function AnnouncementPanel() {
const { notificationsCards, logNotificationImpression, groupNotifications, onClickNotif } =
useNotifications();

const timeoutByUUID = useRef<Record<string, NodeJS.Timeout>>({});
const handleInViewNotif = useCallback(
(visible: boolean, uuid: keyof typeof timeoutByUUID.current) => {
const timeouts = timeoutByUUID.current;

if (notificationsCards.find(n => !n.viewed && n.id === uuid) && visible && !timeouts[uuid]) {
timeouts[uuid] = setTimeout(() => {
logNotificationImpression(uuid);
delete timeouts[uuid];
}, 2000);
}
if (!visible && timeouts[uuid]) {
clearTimeout(timeouts[uuid]);
delete timeouts[uuid];
}
},
[logNotificationImpression, notificationsCards],
);
const { notificationsCards, groupNotifications, onClickNotif } = useNotifications();

const groups = useMemo(
() => groupNotifications(notificationsCards),
Expand Down Expand Up @@ -305,12 +285,8 @@ export function AnnouncementPanel() {
<React.Fragment key={index}>
{group.day ? <DateRow date={group.day} /> : null}
{group.data.map(({ title, description, path, url, viewed, id, cta }, index) => (
<React.Fragment key={id}>
<InView
as="div"
onChange={visible => handleInViewNotif(visible, id)}
onClick={() => onClickNotif(group.data[index])}
>
<div key={id} onClick={() => onClickNotif(group.data[index])}>
<LogContentCardWrapper id={id}>
<Article
title={title}
text={description}
Expand All @@ -322,9 +298,9 @@ export function AnnouncementPanel() {
href: url || path || urls.ledger,
}}
/>
</InView>
</LogContentCardWrapper>
{index < group.data.length - 1 ? <Separator /> : null}
</React.Fragment>
</div>
))}
</React.Fragment>
))}
Expand Down
30 changes: 2 additions & 28 deletions apps/ledger-live-desktop/src/renderer/hooks/useActionCards.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import React, { useEffect, useState } from "react";
import { useEffect, useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import ActionCard from "~/renderer/components/ContentCards/ActionCard";
import { actionContentCardSelector } from "~/renderer/reducers/dynamicContent";
import * as braze from "@braze/web-sdk";
import { setActionCards } from "~/renderer/actions/dynamicContent";
Expand All @@ -23,11 +22,6 @@ const useActionCards = () => {
const findCard = (cardId: string) => cachedContentCards.find(card => card.id === cardId);
const findActionCard = (cardId: string) => actionCards.find(card => card.id === cardId);

const onImpression = (cardId: string) => {
const currentCard = findCard(cardId);
isTrackedUser && currentCard && braze.logContentCardImpressions([currentCard]);
};

const onDismiss = (cardId: string) => {
const currentCard = findCard(cardId);
const actionCard = findActionCard(cardId);
Expand Down Expand Up @@ -79,27 +73,7 @@ const useActionCards = () => {
}
};

const slides = actionCards.map(slide => (
<ActionCard
key={slide.id}
img={slide.image}
title={slide.title}
description={slide.description}
actions={{
primary: {
label: slide.mainCta,
action: () => onClick(slide.id, slide.link),
},
dismiss: {
label: slide.secondaryCta,
action: () => onDismiss(slide.id),
},
}}
onView={() => onImpression(slide.id)}
/>
));

return slides;
return { onClick, onDismiss, actionCards };
};

export default useActionCards;
Loading

0 comments on commit 34cf0f9

Please sign in to comment.