Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

feat: analytics events for credits/awards #4291

Merged
merged 5 commits into from
Mar 18, 2025
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/shared/src/components/award/AwardButton.tsx
Original file line number Diff line number Diff line change
@@ -20,12 +20,14 @@ import type {
} from '../../contexts/GiveAwardModalContext';
import { useRequestProtocol } from '../../hooks/useRequestProtocol';
import { getCompanionWrapper } from '../../lib/extension';
import type { Post } from '../../graphql/posts';

type AwardButtonProps = {
appendTo?: 'parent' | Element | ((ref: Element) => Element);
type: AwardTypes;
className?: string;
entity: AwardEntity;
post?: Post;
} & Pick<ButtonProps<'button'>, 'pressed' | 'variant'>;
export const AwardButton = ({
appendTo: appendToProps,
@@ -34,6 +36,7 @@ export const AwardButton = ({
entity,
pressed,
variant = ButtonVariant.Tertiary,
post,
}: AwardButtonProps): ReactElement => {
const { isCompanion } = useRequestProtocol();
const { user, showLogin } = useAuthContext();
@@ -49,6 +52,7 @@ export const AwardButton = ({
props: {
type,
entity,
post,
},
});
};
Original file line number Diff line number Diff line change
@@ -365,6 +365,7 @@ export default function CommentActionButtons({
numAwards: comment.numAwards,
}}
pressed={!!comment.userState?.awarded}
post={post}
/>
{!!comment.numAwards && (
<Typography
12 changes: 10 additions & 2 deletions packages/shared/src/components/cores/CoreOptionButton.tsx
Original file line number Diff line number Diff line change
@@ -10,6 +10,8 @@ import {
TypographyColor,
TypographyType,
} from '../typography/Typography';
import { LogEvent } from '../../lib/log';
import { useLogContext } from '../../contexts/LogContext';

type CoreOptionButtonProps = {
id: string;
@@ -18,14 +20,20 @@ export const CoreOptionButton = ({
id,
}: CoreOptionButtonProps): ReactElement => {
const isMobile = useViewSize(ViewSize.MobileL);
const { selectedProduct, setSelectedProduct, openCheckout } =
const { logEvent } = useLogContext();
const { selectedProduct, setSelectedProduct, openCheckout, origin } =
useBuyCoresContext();
const onSelect = useCallback(() => {
// TODO: Amount should be deducted from selected product entity
logEvent({
event_name: LogEvent.SelectCreditsQuantity,
extra: JSON.stringify({ origin, amount: id }),
});
setSelectedProduct(id);
if (!isMobile) {
openCheckout({ priceId: id });
}
}, [id, isMobile, openCheckout, setSelectedProduct]);
}, [id, isMobile, logEvent, openCheckout, origin, setSelectedProduct]);
return (
<Button
className={classNames(
13 changes: 12 additions & 1 deletion packages/shared/src/components/credit/BuyCreditsButton.tsx
Original file line number Diff line number Diff line change
@@ -7,6 +7,8 @@ import Link from '../utilities/Link';
import { webappUrl } from '../../lib/constants';
import { anchorDefaultRel } from '../../lib/strings';
import { isIOSNative } from '../../lib/func';
import { LogEvent, Origin } from '../../lib/log';
import { useLogContext } from '../../contexts/LogContext';

type BuyCreditsButtonProps = {
onPlusClick: () => void;
@@ -17,6 +19,15 @@ export const BuyCreditsButton = ({
hideBuyButton,
}: BuyCreditsButtonProps): ReactElement => {
const renderBuyButton = !isIOSNative() && !hideBuyButton;
const { logEvent } = useLogContext();
const trackBuyCredits = () => {
logEvent({
event_name: LogEvent.StartBuyingCredits,
extra: JSON.stringify({ origin: Origin.Award }),
});
onPlusClick();
};

return (
<div className="flex items-center rounded-10 bg-surface-float">
<Link href={`${webappUrl}/earnings`} passHref>
@@ -38,7 +49,7 @@ export const BuyCreditsButton = ({
variant={ButtonVariant.Tertiary}
icon={<PlusIcon />}
size={ButtonSize.Small}
onClick={onPlusClick}
onClick={trackBuyCredits}
/>
</>
) : null}
Original file line number Diff line number Diff line change
@@ -24,6 +24,7 @@ import { IconSize } from '../../Icon';
import useDebounceFn from '../../../hooks/useDebounceFn';
import { CoreOptionList } from '../../cores/CoreOptionList';
import { CoreAmountNeeded } from '../../cores/CoreAmountNeeded';
import type { Origin } from '../../../lib/log';

const CoreOptions = () => {
return (
@@ -182,14 +183,20 @@ const ModalRender = ({ ...props }: ModalProps) => {
};

type BuyCoresModalProps = ModalProps & {
origin: Origin;
onCompletion?: () => void;
};
export const BuyCoresModal = ({
origin,
onCompletion,
...props
}: BuyCoresModalProps): ReactElement => {
return (
<BuyCoresContextProvider amountNeeded={40} onCompletion={onCompletion}>
<BuyCoresContextProvider
amountNeeded={40}
onCompletion={onCompletion}
origin={origin}
>
<ModalRender {...props} />
</BuyCoresContextProvider>
);
67 changes: 52 additions & 15 deletions packages/shared/src/components/modals/award/GiveAwardModal.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { ReactElement } from 'react';
import React, { useCallback, useState } from 'react';
import React, { useCallback, useEffect, useRef, useState } from 'react';

import classNames from 'classnames';
import { useMutation, useQuery } from '@tanstack/react-query';
@@ -38,15 +38,22 @@ import { labels } from '../../../lib';
import type { ApiErrorResult } from '../../../graphql/common';
import { generateQueryKey, RequestKey } from '../../../lib/query';
import { useAuthContext } from '../../../contexts/AuthContext';
import { Origin } from '../../../lib/log';
import type { Post } from '../../../graphql/posts';

const AwardItem = ({ item }: { item: Product }) => {
const { setActiveStep } = useGiveAwardModalContext();
const { setActiveStep, logAwardEvent } = useGiveAwardModalContext();

const onClick = useCallback(() => {
logAwardEvent({ awardEvent: 'PICK', extra: { award: item.value } });
setActiveStep({ screen: 'COMMENT', product: item });
}, [item, logAwardEvent, setActiveStep]);

return (
<Button
variant={ButtonVariant.Float}
className="flex !h-auto flex-col items-center justify-center gap-2 rounded-14 bg-surface-float !p-1"
onClick={() => setActiveStep({ screen: 'COMMENT', product: item })}
onClick={onClick}
>
<Image src={item.image} alt={item.name} className="size-20" />
<div className="flex items-center justify-center">
@@ -133,8 +140,14 @@ const IntroScreen = () => {

const CommentScreen = () => {
const { updateUser, user } = useAuthContext();
const { setActiveStep, type, entity, product, onRequestClose } =
useGiveAwardModalContext();
const {
setActiveStep,
type,
entity,
product,
onRequestClose,
logAwardEvent,
} = useGiveAwardModalContext();
const isMobile = useViewSize(ViewSize.MobileL);
const { displayToast } = useToastNotification();
const [note, setNote] = useState('');
@@ -163,6 +176,24 @@ const CommentScreen = () => {
},
});

const onAwardClick = useCallback(() => {
logAwardEvent({ awardEvent: 'AWARD', extra: { award: product.value } });
awardMutation({
productId: product.id,
type,
entityId: entity.id,
note,
});
}, [
awardMutation,
entity.id,
logAwardEvent,
note,
product.id,
product.value,
type,
]);

return (
<>
<Modal.Header title="Give an Award" showCloseButton={!isMobile}>
@@ -223,14 +254,7 @@ const CommentScreen = () => {
loading={isPending}
className="w-full"
variant={ButtonVariant.Primary}
onClick={() => {
awardMutation({
productId: product.id,
type,
entityId: entity.id,
note,
});
}}
onClick={onAwardClick}
>
Send Award for <CoinIcon />{' '}
{product.value === 0 ? 'Free' : product.value}
@@ -269,7 +293,15 @@ const ModalBody = () => {

const ModalRender = ({ ...props }: ModalProps) => {
const isMobile = useViewSize(ViewSize.MobileL);
const { activeModal, setActiveModal } = useGiveAwardModalContext();
const { activeModal, setActiveModal, logAwardEvent } =
useGiveAwardModalContext();
const trackingRef = useRef(false);
useEffect(() => {
if (!trackingRef.current) {
trackingRef.current = true;
logAwardEvent({ awardEvent: 'START' });
}
}, [logAwardEvent]);

const onCompletion = useCallback(() => {
setActiveModal('AWARD');
@@ -288,7 +320,11 @@ const ModalRender = ({ ...props }: ModalProps) => {
</Modal>
) : null}
{activeModal === 'BUY_CORES' ? (
<BuyCoresModal {...props} onCompletion={onCompletion} />
<BuyCoresModal
{...props}
onCompletion={onCompletion}
origin={Origin.Award}
/>
) : null}
</>
);
@@ -297,6 +333,7 @@ const ModalRender = ({ ...props }: ModalProps) => {
type GiveAwardModalProps = ModalProps & {
type: AwardTypes;
entity: AwardEntity;
post?: Post;
};
const GiveAwardModal = (props: GiveAwardModalProps): ReactElement => {
return (
6 changes: 6 additions & 0 deletions packages/shared/src/contexts/BuyCoresContext.tsx
Original file line number Diff line number Diff line change
@@ -15,6 +15,7 @@ import {
import { checkIsExtension } from '../lib/func';
import type { OpenCheckoutFn } from './payment/context';
import { useAuthContext } from './AuthContext';
import type { Origin } from '../lib/log';

const SCREENS = {
INTRO: 'INTRO',
@@ -32,19 +33,22 @@ export type BuyCoresContextData = {
setSelectedProduct: (product: string) => void;
activeStep: Screens;
setActiveStep: (step: Screens) => void;
origin?: Origin;
};

const BuyCoresContext = React.createContext<BuyCoresContextData>(undefined);
export default BuyCoresContext;

export type BuyCoresContextProviderProps = {
children?: ReactNode;
origin: Origin;
amountNeeded?: number;
onCompletion?: () => void;
};

export const BuyCoresContextProvider = ({
onCompletion,
origin,
amountNeeded,
children,
}: BuyCoresContextProviderProps): ReactElement => {
@@ -121,12 +125,14 @@ export const BuyCoresContextProvider = ({
selectedProduct,
setSelectedProduct,
openCheckout,
origin,
}),
[
activeStep,
amountNeeded,
onCompletion,
openCheckout,
origin,
paddle,
selectedProduct,
],
74 changes: 72 additions & 2 deletions packages/shared/src/contexts/GiveAwardModalContext.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import type { Dispatch, ReactElement, ReactNode, SetStateAction } from 'react';
import React, { useContext, useMemo, useState } from 'react';
import React, { useCallback, useContext, useMemo, useState } from 'react';
import type { ModalProps } from '../components/modals/common/Modal';
import type { Product } from '../graphql/njord';
import type { PublicProfile } from '../lib/user';
import { useLogContext } from './LogContext';
import { postLogEvent } from '../lib/feed';
import { LogEvent } from '../lib/log';
import type { Post } from '../graphql/posts';

const AWARD_TYPES = {
USER: 'USER',
@@ -24,6 +28,34 @@ export type ModalRenders = keyof typeof MODALRENDERS;

export type Screens = keyof typeof SCREENS;

const AWARD_EVENTS = {
START: 'START',
PICK: 'PICK',
AWARD: 'AWARD',
};
type AwardEvents = keyof typeof AWARD_EVENTS;

const AwardTypeToTrackingEvent: Record<
AwardTypes,
Record<AwardEvents, LogEvent>
> = {
USER: {
START: LogEvent.StartAwardUser,
PICK: LogEvent.PickAwardUser,
AWARD: LogEvent.AwardUser,
},
POST: {
START: LogEvent.StartAwardPost,
PICK: LogEvent.PickAwardPost,
AWARD: LogEvent.AwardPost,
},
COMMENT: {
START: LogEvent.StartAwardComment,
PICK: LogEvent.PickAwardComment,
AWARD: LogEvent.AwardComment,
},
};

export const maxNoteLength = 400;

export type AwardEntity = {
@@ -42,6 +74,10 @@ export type GiveAwardModalContextData = {
type: AwardTypes;
entity: AwardEntity;
product?: Product;
logAwardEvent: (args: {
awardEvent: AwardEvents;
extra?: Record<string, unknown>;
}) => void;
} & Pick<ModalProps, 'onRequestClose'>;

const GiveAwardModalContext =
@@ -52,14 +88,17 @@ export type GiveAwardModalContextProviderProps = {
children?: ReactNode;
type: AwardTypes;
entity: AwardEntity;
post?: Post;
} & Pick<ModalProps, 'onRequestClose'>;

export const GiveAwardModalContextProvider = ({
children,
onRequestClose,
type,
entity,
post,
}: GiveAwardModalContextProviderProps): ReactElement => {
const { logEvent } = useLogContext();
const [activeStep, setActiveStep] = useState<{
screen: Screens;
product?: Product;
@@ -70,6 +109,28 @@ export const GiveAwardModalContextProvider = ({
MODALRENDERS.AWARD,
);

const logAwardEvent = useCallback(
({
awardEvent,
extra,
}: {
awardEvent: AwardEvents;
extra?: Record<string, unknown>;
}): void => {
const eventName = AwardTypeToTrackingEvent[type]?.[awardEvent];
if (type === 'USER') {
// User is a non post event
logEvent({
event_name: eventName,
extra: JSON.stringify({ ...extra }),
});
} else {
logEvent(postLogEvent(eventName, post, { extra }));
}
},
[logEvent, post, type],
);

const contextData = useMemo<GiveAwardModalContextData>(
() => ({
activeModal,
@@ -80,8 +141,17 @@ export const GiveAwardModalContextProvider = ({
setActiveStep,
type,
entity,
logAwardEvent,
}),
[activeModal, activeStep, onRequestClose, type, entity],
[
activeModal,
onRequestClose,
activeStep.screen,
activeStep.product,
type,
entity,
logAwardEvent,
],
);

return (
18 changes: 18 additions & 0 deletions packages/shared/src/lib/log.ts
Original file line number Diff line number Diff line change
@@ -62,6 +62,11 @@ export enum Origin {
// Marketing
InAppPromotion = 'in app promotion',
Suggestions = 'suggestions',
// Start Credits
EarningsPageCTA = 'earnings page cta',
EarningsPagePackage = 'earnings page package',
Award = 'award',
// End Credits
}

export enum LogEvent {
@@ -253,6 +258,19 @@ export enum LogEvent {
UpdateProfile = 'update profile',
UpdateProfileImage = 'update profile image',
// End Profile
// Start Credits
StartBuyingCredits = 'start buying credits',
SelectCreditsQuantity = 'select credits quantity',
StartAwardUser = 'start award user',
PickAwardUser = 'pick award user',
AwardUser = 'award user',
StartAwardPost = 'start award post',
PickAwardPost = 'pick award post',
AwardPost = 'award post',
StartAwardComment = 'start award comment',
PickAwardComment = 'pick award comment',
AwardComment = 'award comment',
// End Credits
}

export enum FeedItemTitle {
4 changes: 3 additions & 1 deletion packages/webapp/pages/cores/index.tsx
Original file line number Diff line number Diff line change
@@ -18,6 +18,7 @@ import {
} from '@dailydotdev/shared/src/components/buttons/Button';
import { CoinIcon } from '@dailydotdev/shared/src/components/icons';
import { CoreFAQ } from '@dailydotdev/shared/src/components/cores/CoreFAQ';
import { Origin } from '@dailydotdev/shared/src/lib/log';
import { useAuthContext } from '@dailydotdev/shared/src/contexts/AuthContext';
import { getTemplatedTitle } from '../../components/layouts/utils';
import { defaultOpenGraph } from '../../next-seo';
@@ -81,7 +82,8 @@ const CoresPage = (): ReactElement => {
}

return (
<BuyCoresContextProvider>
// TODO: Take correct origin from referrer
<BuyCoresContextProvider origin={Origin.EarningsPageCTA}>
{isLaptop ? <CorePageDesktop /> : <CorePageMobile />}
</BuyCoresContextProvider>
);
105 changes: 86 additions & 19 deletions packages/webapp/pages/earnings.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { ReactElement } from 'react';
import React from 'react';
import React, { useCallback } from 'react';
import type { NextSeoProps } from 'next-seo';
import type { WithClassNameProps } from '@dailydotdev/shared/src/components/utilities';
import {
@@ -18,7 +18,11 @@ import {
TypographyTag,
TypographyType,
} from '@dailydotdev/shared/src/components/typography/Typography';
import { docs, searchDocs } from '@dailydotdev/shared/src/lib/constants';
import {
docs,
searchDocs,
webappUrl,
} from '@dailydotdev/shared/src/lib/constants';
import {
CoinIcon,
DevPlusIcon,
@@ -42,28 +46,50 @@ import {
} from '@dailydotdev/shared/src/components/ProfilePicture';
import { TimeFormatType } from '@dailydotdev/shared/src/lib/dateFormat';
import { Separator } from '@dailydotdev/shared/src/components/cards/common/common';
import Link from '@dailydotdev/shared/src/components/utilities/Link';
import { LogEvent, Origin } from '@dailydotdev/shared/src/lib/log';
import { useLogContext } from '@dailydotdev/shared/src/contexts/LogContext';
import { getLayout as getFooterNavBarLayout } from '../components/layouts/FooterNavBarLayout';
import { getLayout } from '../components/layouts/MainLayout';
import ProtectedPage from '../components/ProtectedPage';

type LogStartBuyingCreditsProps = {
origin: Origin;
target_id?: string;
amount?: number;
};

type BuyCoreProps = {
onBuyCoresClick: (props: LogStartBuyingCreditsProps) => void;
amount: number;
price: number;
};
const BuyCore = ({ amount, price }: BuyCoreProps): ReactElement => {
const BuyCore = ({
onBuyCoresClick,
amount,
price,
}: BuyCoreProps): ReactElement => {
return (
<div className="flex flex-1 flex-col items-center rounded-14 bg-surface-float p-2">
<CoinIcon size={IconSize.XLarge} className="mb-1" />
<Typography type={TypographyType.Title3} bold>
{amount}
</Typography>
<Typography
type={TypographyType.Caption2}
color={TypographyColor.Tertiary}
<Link href={`${webappUrl}/cores`}>
<a
href={`${webappUrl}/cores`}
className="flex flex-1 flex-col items-center rounded-14 bg-surface-float p-2"
onClick={() =>
onBuyCoresClick({ amount, origin: Origin.EarningsPagePackage })
}
>
${price}
</Typography>
</div>
<CoinIcon size={IconSize.XLarge} className="mb-1" />
<Typography type={TypographyType.Title3} bold>
{amount}
</Typography>
<Typography
type={TypographyType.Caption2}
color={TypographyColor.Tertiary}
>
${price}
</Typography>
</a>
</Link>
);
};

@@ -166,6 +192,22 @@ const TransactionItem = ({
};

const Earnings = (): ReactElement => {
const { logEvent } = useLogContext();
const onBuyCoresClick = useCallback(
({
origin = Origin.EarningsPageCTA,
amount,
target_id,
}: Partial<LogStartBuyingCreditsProps>) => {
logEvent({
event_name: LogEvent.StartBuyingCredits,
target_id,
extra: JSON.stringify({ origin, amount }),
});
},
[logEvent],
);

return (
<ProtectedPage>
<div className="m-auto flex w-full max-w-screen-laptop flex-col pb-12 tablet:pb-0 laptop:min-h-page laptop:flex-row laptop:border-l laptop:border-r laptop:border-border-subtlest-tertiary laptop:pb-6 laptopL:pb-0">
@@ -174,7 +216,13 @@ const Earnings = (): ReactElement => {
<Typography type={TypographyType.Title3} bold>
Core wallet
</Typography>
<Button size={ButtonSize.Small} variant={ButtonVariant.Primary}>
<Button
size={ButtonSize.Small}
variant={ButtonVariant.Primary}
onClick={() => onBuyCoresClick({ target_id: 'Buy Cores' })}
tag="a"
href={`${webappUrl}/cores`}
>
Buy Cores
</Button>
</header>
@@ -333,11 +381,30 @@ const Earnings = (): ReactElement => {
</Typography>
</div>
<div className="flex gap-3">
<BuyCore amount={100} price={3.99} />
<BuyCore amount={500} price={50} />
<BuyCore amount={1000} price={100} />
<BuyCore
onBuyCoresClick={onBuyCoresClick}
amount={100}
price={3.99}
/>
<BuyCore
onBuyCoresClick={onBuyCoresClick}
amount={500}
price={50}
/>
<BuyCore
onBuyCoresClick={onBuyCoresClick}
amount={1000}
price={100}
/>
</div>
<Button variant={ButtonVariant.Float}>See more options</Button>
<Button
variant={ButtonVariant.Float}
onClick={() => onBuyCoresClick({ target_id: 'See more options' })}
tag="a"
href={`${webappUrl}/cores`}
>
See more options
</Button>
</WidgetContainer>
<WidgetContainer className="flex flex-col gap-4 p-6">
<div className="flex justify-between">