posthog migration: part 4 (#7366)
# Analytics Event Reporting System
This PR introduces a structured analytics event reporting system for the dashboard app using PostHog. It provides clear guidelines and standardized patterns for tracking user actions.
## Key Features
- Added a centralized analytics reporting system in `src/@/analytics/report.ts`
- Implemented standardized naming conventions for events (`<subject> <verb>`) and reporting functions (`report<Subject><Verb>`)
- Created comprehensive documentation in `apps/dashboard/src/@/analytics/README.md`
- Added mandatory JSDoc requirements to document the purpose and ownership of each event
- Integrated the first event: `reportContractDeployed` to track contract deployments
- Updated the contract deployment form to use the new analytics system
## Guidelines
- Events should only be added when they answer concrete product/business questions
- All events must include typed properties and proper documentation
- Analytics code is client-side only (never import `posthog-js` in server components)
- Developers should notify #core-services before renaming or removing events
This standardized approach will make our analytics more maintainable, discoverable, and purposeful.
<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit
- **Documentation**
- Added comprehensive guidelines and a README for analytics event reporting, including naming conventions, usage examples, and requirements for event tracking in the dashboard app.
- **New Features**
- Introduced new analytics events covering contract deployments and multiple stages of team onboarding, such as plan selection, member invitations, and onboarding completion.
- **Refactor**
- Centralized analytics reporting by replacing generic event tracking with dedicated reporting functions across contract deployment and team onboarding flows.
- Removed redundant and granular event tracking calls, simplifying analytics integration and improving consistency.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
<!-- start pr-codex -->
---
## PR-Codex overview
This PR focuses on enhancing analytics event reporting for onboarding and contract deployment processes across the application, ensuring better tracking of user interactions and feedback.
### Detailed summary
- Added `.eslintignore` entries for markdown files.
- Updated `InviteTeamMembers` and `InviteSection` to report success counts on invites.
- Introduced new reporting functions in `report.ts` for onboarding and contract events.
- Removed unused `trackEvent` calls, replacing them with specific reporting functions.
- Enhanced documentation for analytics guidelines and event reporting in `README.md` files.
> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`
<!-- end pr-codex -->
This commit is contained in:
@@ -3,7 +3,6 @@ description: Rules for writing features in apps/dashboard
|
||||
globs: dashboard
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# Reusable Core UI Components
|
||||
|
||||
- Always import from the central UI library under `@/components/ui/*` – e.g. `import { Button } from "@/components/ui/button"`.
|
||||
@@ -101,3 +100,29 @@ Guidelines:
|
||||
- Keep `queryKey` stable and descriptive for cache hits.
|
||||
- Prefer API routes or server actions to keep tokens secret; the browser only sees relative paths.
|
||||
- Configure `staleTime` / `cacheTime` according to freshness requirements.
|
||||
|
||||
# Analytics Event Reporting
|
||||
|
||||
- **Add events intentionally** – only when they answer a concrete product/business question.
|
||||
- **Event name**: human-readable `<subject> <verb>` phrase (e.g. `"contract deployed"`).
|
||||
- **Reporting helper**: `report<Subject><Verb>` (PascalCase); all live in `src/@/analytics/report.ts`.
|
||||
- **Mandatory JSDoc**: explain *Why* the event exists and *Who* owns it (`@username`).
|
||||
- **Typed properties**: accept a single `properties` object and pass it unchanged to `posthog.capture`.
|
||||
- **Client-side only**: never import `posthog-js` in server components.
|
||||
- **Housekeeping**: ping **#core-services** before renaming or removing an event.
|
||||
|
||||
```ts
|
||||
/**
|
||||
* ### Why do we need to report this event?
|
||||
* - Tracks number of contracts deployed
|
||||
*
|
||||
* ### Who is responsible for this event?
|
||||
* @jnsdls
|
||||
*/
|
||||
export function reportContractDeployed(properties: {
|
||||
address: string;
|
||||
chainId: number;
|
||||
}) {
|
||||
posthog.capture("contract deployed", properties);
|
||||
}
|
||||
```
|
||||
|
||||
@@ -104,6 +104,41 @@ Welcome, AI copilots! This guide captures the coding standards, architectural de
|
||||
-- Configure staleTime / cacheTime based on freshness requirements (default ≥ 60 s).
|
||||
-- Keep tokens secret by calling internal API routes or server actions.
|
||||
|
||||
6.5 Analytics Event Reporting
|
||||
|
||||
- **When to create a new event**
|
||||
-- Only add events that answer a clear product or business question.
|
||||
-- Check `src/@/analytics/report.ts` first; avoid duplicates.
|
||||
|
||||
- **Naming conventions**
|
||||
-- **Event name**: human-readable phrase in the form `<subject> <verb>` (e.g. "contract deployed").
|
||||
-- **Reporting function**: `report<Subject><Verb>` (PascalCase).
|
||||
-- All reporting helpers currently live in the shared `report.ts` file.
|
||||
|
||||
- **Boilerplate template**
|
||||
-- Add a JSDoc header explaining **Why** the event exists and **Who** owns it (`@username`).
|
||||
-- Accept a single typed `properties` object and forward it unchanged to `posthog.capture`.
|
||||
-- Example:
|
||||
|
||||
```ts
|
||||
/**
|
||||
* ### Why do we need to report this event?
|
||||
* - Tracks number of contracts deployed
|
||||
*
|
||||
* ### Who is responsible for this event?
|
||||
* @jnsdls
|
||||
*/
|
||||
export function reportContractDeployed(properties: {
|
||||
address: string;
|
||||
chainId: number;
|
||||
}) {
|
||||
posthog.capture("contract deployed", properties);
|
||||
}
|
||||
```
|
||||
|
||||
- **Client-side only**: never import `posthog-js` in server components.
|
||||
- **Housekeeping**: Inform **#eng-core-services** before renaming or removing an existing event.
|
||||
|
||||
⸻
|
||||
|
||||
7. Performance & Bundle Size
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
# Analytics Guidelines
|
||||
|
||||
This folder centralises the **PostHog** tracking logic for the dashboard app.
|
||||
Most developers will only need to add or extend _event-reporting_ functions in `report.ts`.
|
||||
|
||||
---
|
||||
|
||||
## 1. When to add an event
|
||||
1. Ask yourself if the data will be **actionable**. Every event should have a clear product or business question it helps answer.
|
||||
2. Check if a similar event already exists in `report.ts`. Avoid duplicates.
|
||||
|
||||
---
|
||||
|
||||
## 2. Naming conventions
|
||||
| Concept | Convention | Example |
|
||||
|---------|------------|---------|
|
||||
| **Event name** (string sent to PostHog) | Human-readable phrase formatted as `<subject> <verb>` | `"contract deployed"` |
|
||||
| **Reporting function** | `report<Subject><Verb>` (PascalCase) | `reportContractDeployed` |
|
||||
| **File** | All event functions live in the shared `report.ts` file (for now) | — |
|
||||
|
||||
> Keeping names predictable makes it easy to search both code and analytics.
|
||||
|
||||
---
|
||||
|
||||
## 3. Boilerplate / template
|
||||
Add a new function to `report.ts` following this pattern:
|
||||
|
||||
```ts
|
||||
/**
|
||||
* ### Why do we need to report this event?
|
||||
* - _Add bullet points explaining the product metrics/questions this event answers._
|
||||
*
|
||||
* ### Who is responsible for this event?
|
||||
* @your-github-handle
|
||||
*/
|
||||
export function reportExampleEvent(properties: {
|
||||
/* Add typed properties here */
|
||||
}) {
|
||||
posthog.capture("example event", {
|
||||
/* Pass the same properties here */
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
Guidelines:
|
||||
1. **Explain the "why".** The JSDoc block is mandatory so future contributors know the purpose.
|
||||
2. **Type everything.** The `properties` object should be fully typed—this doubles as documentation.
|
||||
3. **Client-side only.** `posthog-js` must never run on the server. Call these reporting helpers from client components, event handlers, etc.
|
||||
|
||||
---
|
||||
|
||||
## 4. Editing or removing events
|
||||
1. Update both the function and the PostHog event definition (if required).
|
||||
2. Inform the core services team before removing or renaming an event.
|
||||
|
||||
---
|
||||
|
||||
## 5. Identification & housekeeping (FYI)
|
||||
Most devs can ignore this section, but for completeness:
|
||||
|
||||
- `hooks/identify-account.ts` and `hooks/identify-team.ts` wrap `posthog.identify`/`group` calls.
|
||||
- `resetAnalytics` clears identity state (used on logout).
|
||||
|
||||
---
|
||||
|
||||
## 6. Need help?
|
||||
Ping #eng-core-services in slack.
|
||||
@@ -0,0 +1,150 @@
|
||||
import posthog from "posthog-js";
|
||||
|
||||
import type { Team } from "../api/team";
|
||||
|
||||
// ----------------------------
|
||||
// CONTRACTS
|
||||
// ----------------------------
|
||||
|
||||
/**
|
||||
* ### Why do we need to report this event?
|
||||
* - To track the number of contracts deployed
|
||||
* - To track the number of contracts deployed on each chain
|
||||
*
|
||||
* ### Who is responsible for this event?
|
||||
* @jnsdls
|
||||
*
|
||||
*/
|
||||
export function reportContractDeployed(properties: {
|
||||
address: string;
|
||||
chainId: number;
|
||||
publisher: string | undefined;
|
||||
contractName: string | undefined;
|
||||
}) {
|
||||
posthog.capture("contract deployed", properties);
|
||||
}
|
||||
|
||||
/**
|
||||
* ### Why do we need to report this event?
|
||||
* - To track the number of contracts that failed to deploy
|
||||
* - To track the error message of the failed contract deployment (so we can fix it / add workarounds)
|
||||
*
|
||||
* ### Who is responsible for this event?
|
||||
* @jnsdls
|
||||
*
|
||||
*/
|
||||
export function reportContractDeployFailed(properties: {
|
||||
errorMessage: string;
|
||||
}) {
|
||||
posthog.capture("contract deploy failed", properties);
|
||||
}
|
||||
|
||||
// ----------------------------
|
||||
// ONBOARDING (TEAM)
|
||||
// ----------------------------
|
||||
|
||||
/**
|
||||
* ### Why do we need to report this event?
|
||||
* - To track the number of teams that enter the onboarding flow
|
||||
*
|
||||
* ### Who is responsible for this event?
|
||||
* @jnsdls
|
||||
*
|
||||
*/
|
||||
export function reportOnboardingStarted() {
|
||||
posthog.capture("onboarding started");
|
||||
}
|
||||
|
||||
/**
|
||||
* ### Why do we need to report this event?
|
||||
* - To track the number of teams that select a paid plan during onboarding
|
||||
* - To know **which** plan was selected
|
||||
*
|
||||
* ### Who is responsible for this event?
|
||||
* @jnsdls
|
||||
*
|
||||
*/
|
||||
export function reportOnboardingPlanSelected(properties: {
|
||||
plan: Team["billingPlan"];
|
||||
}) {
|
||||
posthog.capture("onboarding plan selected", properties);
|
||||
}
|
||||
|
||||
/**
|
||||
* ### Why do we need to report this event?
|
||||
* - To track the number of teams that skip the plan-selection step during onboarding
|
||||
*
|
||||
* ### Who is responsible for this event?
|
||||
* @jnsdls
|
||||
*
|
||||
*/
|
||||
export function reportOnboardingPlanSelectionSkipped() {
|
||||
posthog.capture("onboarding plan selection skipped");
|
||||
}
|
||||
|
||||
/**
|
||||
* ### Why do we need to report this event?
|
||||
* - To track the number of teams that invite members during onboarding
|
||||
* - To track **how many** members were invited
|
||||
*
|
||||
* ### Who is responsible for this event?
|
||||
* @jnsdls
|
||||
*
|
||||
*/
|
||||
export function reportOnboardingMembersInvited(properties: {
|
||||
count: number;
|
||||
}) {
|
||||
posthog.capture("onboarding members invited", {
|
||||
count: properties.count,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* ### Why do we need to report this event?
|
||||
* - To track the number of teams that skip inviting members during onboarding
|
||||
*
|
||||
* ### Who is responsible for this event?
|
||||
* @jnsdls
|
||||
*
|
||||
*/
|
||||
export function reportOnboardingMembersSkipped() {
|
||||
posthog.capture("onboarding members skipped");
|
||||
}
|
||||
|
||||
/**
|
||||
* ### Why do we need to report this event?
|
||||
* - To track how many teams click the upsell (upgrade) button on the member-invite step during onboarding
|
||||
*
|
||||
* ### Who is responsible for this event?
|
||||
* @jnsdls
|
||||
*
|
||||
*/
|
||||
export function reportOnboardingMembersUpsellButtonClicked() {
|
||||
posthog.capture("onboarding members upsell clicked");
|
||||
}
|
||||
|
||||
/**
|
||||
* ### Why do we need to report this event?
|
||||
* - To track which plan is selected from the members-step upsell during onboarding
|
||||
*
|
||||
* ### Who is responsible for this event?
|
||||
* @jnsdls
|
||||
*
|
||||
*/
|
||||
export function reportOnboardingMembersUpsellPlanSelected(properties: {
|
||||
plan: Team["billingPlan"];
|
||||
}) {
|
||||
posthog.capture("onboarding members upsell plan selected", properties);
|
||||
}
|
||||
|
||||
/**
|
||||
* ### Why do we need to report this event?
|
||||
* - To track the number of teams that completed onboarding
|
||||
*
|
||||
* ### Who is responsible for this event?
|
||||
* @jnsdls
|
||||
*
|
||||
*/
|
||||
export function reportOnboardingCompleted() {
|
||||
posthog.capture("onboarding completed");
|
||||
}
|
||||
+11
-31
@@ -1,11 +1,14 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
reportOnboardingPlanSelected,
|
||||
reportOnboardingPlanSelectionSkipped,
|
||||
} from "@/analytics/report";
|
||||
import type { Team } from "@/api/team";
|
||||
import { PricingCard } from "@/components/blocks/pricing-card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { useDashboardRouter } from "@/lib/DashboardRouter";
|
||||
import { useTrack } from "hooks/analytics/useTrack";
|
||||
import Link from "next/link";
|
||||
import { pollWithTimeout } from "utils/pollWithTimeout";
|
||||
import { useStripeRedirectEvent } from "../../../../../(stripe)/stripe-redirect/stripeRedirectChannel";
|
||||
@@ -14,7 +17,6 @@ export function PlanSelector(props: {
|
||||
team: Team;
|
||||
getTeam: () => Promise<Team>;
|
||||
}) {
|
||||
const trackEvent = useTrack();
|
||||
const router = useDashboardRouter();
|
||||
|
||||
useStripeRedirectEvent(async () => {
|
||||
@@ -25,12 +27,6 @@ export function PlanSelector(props: {
|
||||
const isNonFreePlan = team.billingPlan !== "free";
|
||||
|
||||
if (isNonFreePlan) {
|
||||
trackEvent({
|
||||
category: "teamOnboarding",
|
||||
action: "upgradePlan",
|
||||
label: "success",
|
||||
plan: team.billingPlan,
|
||||
});
|
||||
router.replace(`/get-started/team/${props.team.slug}/add-members`);
|
||||
}
|
||||
|
||||
@@ -49,10 +45,7 @@ export function PlanSelector(props: {
|
||||
label: "Get Started",
|
||||
type: "checkout",
|
||||
onClick() {
|
||||
trackEvent({
|
||||
category: "teamOnboarding",
|
||||
action: "selectPlan",
|
||||
label: "attempt",
|
||||
reportOnboardingPlanSelected({
|
||||
plan: "starter",
|
||||
});
|
||||
},
|
||||
@@ -71,10 +64,7 @@ export function PlanSelector(props: {
|
||||
label: "Get Started",
|
||||
type: "checkout",
|
||||
onClick() {
|
||||
trackEvent({
|
||||
category: "teamOnboarding",
|
||||
action: "selectPlan",
|
||||
label: "attempt",
|
||||
reportOnboardingPlanSelected({
|
||||
plan: "growth",
|
||||
});
|
||||
},
|
||||
@@ -94,10 +84,7 @@ export function PlanSelector(props: {
|
||||
label: "Get started",
|
||||
type: "checkout",
|
||||
onClick() {
|
||||
trackEvent({
|
||||
category: "teamOnboarding",
|
||||
action: "selectPlan",
|
||||
label: "attempt",
|
||||
reportOnboardingPlanSelected({
|
||||
plan: "scale",
|
||||
});
|
||||
},
|
||||
@@ -116,10 +103,7 @@ export function PlanSelector(props: {
|
||||
label: "Get started",
|
||||
type: "checkout",
|
||||
onClick() {
|
||||
trackEvent({
|
||||
category: "teamOnboarding",
|
||||
action: "selectPlan",
|
||||
label: "attempt",
|
||||
reportOnboardingPlanSelected({
|
||||
plan: "pro",
|
||||
});
|
||||
},
|
||||
@@ -146,17 +130,13 @@ export function PlanSelector(props: {
|
||||
variant="link"
|
||||
className="self-center text-muted-foreground"
|
||||
asChild
|
||||
onClick={() => {
|
||||
trackEvent({
|
||||
category: "teamOnboarding",
|
||||
action: "selectPlan",
|
||||
label: "skip",
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Link
|
||||
replace
|
||||
href={`/get-started/team/${props.team.slug}/add-members`}
|
||||
onClick={() => {
|
||||
reportOnboardingPlanSelectionSkipped();
|
||||
}}
|
||||
>
|
||||
Skip picking a plan for now and upgrade later
|
||||
</Link>
|
||||
|
||||
@@ -17,11 +17,6 @@ export default async function Page(props: {
|
||||
notFound();
|
||||
}
|
||||
|
||||
// const client = getClientThirdwebClient({
|
||||
// jwt: authToken,
|
||||
// teamId: team.id,
|
||||
// });
|
||||
|
||||
async function getTeam() {
|
||||
"use server";
|
||||
const resolvedTeam = await getTeamBySlug(params.team_slug);
|
||||
|
||||
-3
@@ -62,9 +62,6 @@ function Story(props: {
|
||||
<TeamOnboardingLayout currentStep={2}>
|
||||
<InviteTeamMembersUI
|
||||
client={storybookThirdwebClient}
|
||||
trackEvent={(params) => {
|
||||
storybookLog("trackEvent", params);
|
||||
}}
|
||||
getTeam={async () => {
|
||||
return teamStub("foo", props.plan);
|
||||
}}
|
||||
|
||||
+21
-37
@@ -1,5 +1,11 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
reportOnboardingMembersInvited,
|
||||
reportOnboardingMembersSkipped,
|
||||
reportOnboardingMembersUpsellButtonClicked,
|
||||
reportOnboardingMembersUpsellPlanSelected,
|
||||
} from "@/analytics/report";
|
||||
import type { Team } from "@/api/team";
|
||||
import { PricingCard } from "@/components/blocks/pricing-card";
|
||||
import { Spinner } from "@/components/ui/Spinner/Spinner";
|
||||
@@ -14,7 +20,6 @@ import {
|
||||
} from "@/components/ui/sheet";
|
||||
import { TabButtons } from "@/components/ui/tabs";
|
||||
import { useDashboardRouter } from "@/lib/DashboardRouter";
|
||||
import type { TrackingParams } from "hooks/analytics/useTrack";
|
||||
import { ArrowRightIcon, CircleArrowUpIcon } from "lucide-react";
|
||||
import { useState, useTransition } from "react";
|
||||
import type { ThirdwebClient } from "thirdweb";
|
||||
@@ -30,14 +35,13 @@ export function InviteTeamMembersUI(props: {
|
||||
inviteTeamMembers: InviteTeamMembersFn;
|
||||
onComplete: () => void;
|
||||
getTeam: () => Promise<Team>;
|
||||
trackEvent: (params: TrackingParams) => void;
|
||||
client: ThirdwebClient;
|
||||
}) {
|
||||
const [showPlanModal, setShowPlanModal] = useState(false);
|
||||
const [isPending, startTransition] = useTransition();
|
||||
const router = useDashboardRouter();
|
||||
const [isPollingTeam, setIsPollingTeam] = useState(false);
|
||||
const [hasSentInvites, setHasSentInvites] = useState(false);
|
||||
const [successCount, setSuccessCount] = useState(0);
|
||||
|
||||
const showSpinner = isPollingTeam || isPending;
|
||||
|
||||
@@ -52,15 +56,6 @@ export function InviteTeamMembersUI(props: {
|
||||
const isNonFreePlan =
|
||||
team.billingPlan !== "free" && team.billingPlan !== "starter";
|
||||
|
||||
if (isNonFreePlan) {
|
||||
props.trackEvent({
|
||||
category: "teamOnboarding",
|
||||
action: "upgradePlan",
|
||||
label: "success",
|
||||
plan: team.billingPlan,
|
||||
});
|
||||
}
|
||||
|
||||
return isNonFreePlan;
|
||||
},
|
||||
timeoutMs: 5000,
|
||||
@@ -73,6 +68,8 @@ export function InviteTeamMembersUI(props: {
|
||||
});
|
||||
});
|
||||
|
||||
const hasSentInvites = successCount > 0;
|
||||
|
||||
return (
|
||||
<div className="relative flex grow flex-col">
|
||||
<Sheet open={showPlanModal} onOpenChange={setShowPlanModal}>
|
||||
@@ -80,7 +77,6 @@ export function InviteTeamMembersUI(props: {
|
||||
<InviteModalContent
|
||||
billingStatus={props.team.billingStatus}
|
||||
teamSlug={props.team.slug}
|
||||
trackEvent={props.trackEvent}
|
||||
getTeam={props.getTeam}
|
||||
teamId={props.team.id}
|
||||
/>
|
||||
@@ -91,7 +87,9 @@ export function InviteTeamMembersUI(props: {
|
||||
inviteTeamMembers={props.inviteTeamMembers}
|
||||
team={props.team}
|
||||
userHasEditPermission={true}
|
||||
onInviteSuccess={() => setHasSentInvites(true)}
|
||||
onInviteSuccess={(count) =>
|
||||
setSuccessCount((prevCount) => prevCount + count)
|
||||
}
|
||||
shouldHideInviteButton={hasSentInvites}
|
||||
client={props.client}
|
||||
// its a new team, there's no recommended members
|
||||
@@ -106,11 +104,7 @@ export function InviteTeamMembersUI(props: {
|
||||
className="gap-2"
|
||||
onClick={() => {
|
||||
setShowPlanModal(true);
|
||||
props.trackEvent({
|
||||
category: "teamOnboarding",
|
||||
action: "upgradePlan",
|
||||
label: "openModal",
|
||||
});
|
||||
reportOnboardingMembersUpsellButtonClicked();
|
||||
}}
|
||||
>
|
||||
<CircleArrowUpIcon className="size-4" />
|
||||
@@ -121,11 +115,11 @@ export function InviteTeamMembersUI(props: {
|
||||
<Button
|
||||
onClick={() => {
|
||||
props.onComplete();
|
||||
if (!hasSentInvites) {
|
||||
props.trackEvent({
|
||||
category: "teamOnboarding",
|
||||
action: "inviteTeamMembers",
|
||||
label: "skip",
|
||||
if (successCount === 0) {
|
||||
reportOnboardingMembersSkipped();
|
||||
} else {
|
||||
reportOnboardingMembersInvited({
|
||||
count: successCount,
|
||||
});
|
||||
}
|
||||
}}
|
||||
@@ -152,7 +146,6 @@ export function InviteTeamMembersUI(props: {
|
||||
function InviteModalContent(props: {
|
||||
teamSlug: string;
|
||||
billingStatus: Team["billingStatus"];
|
||||
trackEvent: (params: TrackingParams) => void;
|
||||
getTeam: () => Promise<Team>;
|
||||
teamId: string;
|
||||
}) {
|
||||
@@ -169,10 +162,7 @@ function InviteModalContent(props: {
|
||||
label: "Get Started",
|
||||
type: "checkout",
|
||||
onClick() {
|
||||
props.trackEvent({
|
||||
category: "teamOnboarding",
|
||||
action: "upgradePlan",
|
||||
label: "attempt",
|
||||
reportOnboardingMembersUpsellPlanSelected({
|
||||
plan: "growth",
|
||||
});
|
||||
},
|
||||
@@ -192,10 +182,7 @@ function InviteModalContent(props: {
|
||||
label: "Get started",
|
||||
type: "checkout",
|
||||
onClick() {
|
||||
props.trackEvent({
|
||||
category: "teamOnboarding",
|
||||
action: "upgradePlan",
|
||||
label: "attempt",
|
||||
reportOnboardingMembersUpsellPlanSelected({
|
||||
plan: "scale",
|
||||
});
|
||||
},
|
||||
@@ -214,10 +201,7 @@ function InviteModalContent(props: {
|
||||
label: "Get started",
|
||||
type: "checkout",
|
||||
onClick() {
|
||||
props.trackEvent({
|
||||
category: "teamOnboarding",
|
||||
action: "upgradePlan",
|
||||
label: "attempt",
|
||||
reportOnboardingMembersUpsellPlanSelected({
|
||||
plan: "pro",
|
||||
});
|
||||
},
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
"use client";
|
||||
import { apiServerProxy } from "@/actions/proxies";
|
||||
import { sendTeamInvites } from "@/actions/sendTeamInvite";
|
||||
import {
|
||||
reportOnboardingCompleted,
|
||||
reportOnboardingStarted,
|
||||
} from "@/analytics/report";
|
||||
import type { Team } from "@/api/team";
|
||||
import { useDashboardRouter } from "@/lib/DashboardRouter";
|
||||
import { useTrack } from "hooks/analytics/useTrack";
|
||||
import { useEffect } from "react";
|
||||
import { toast } from "sonner";
|
||||
import type { ThirdwebClient } from "thirdweb";
|
||||
import { upload } from "thirdweb/storage";
|
||||
@@ -17,7 +21,12 @@ export function TeamInfoForm(props: {
|
||||
teamSlug: string;
|
||||
}) {
|
||||
const router = useDashboardRouter();
|
||||
const trackEvent = useTrack();
|
||||
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
useEffect(() => {
|
||||
reportOnboardingStarted();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<TeamInfoFormUI
|
||||
isTeamSlugAvailable={async (slug) => {
|
||||
@@ -48,12 +57,6 @@ export function TeamInfoForm(props: {
|
||||
slug: data.slug,
|
||||
};
|
||||
|
||||
trackEvent({
|
||||
category: "teamOnboarding",
|
||||
action: "updateTeam",
|
||||
label: "attempt",
|
||||
});
|
||||
|
||||
if (data.image) {
|
||||
try {
|
||||
teamValue.image = await upload({
|
||||
@@ -72,21 +75,9 @@ export function TeamInfoForm(props: {
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
trackEvent({
|
||||
category: "teamOnboarding",
|
||||
action: "updateTeam",
|
||||
label: "error",
|
||||
});
|
||||
|
||||
throw new Error(res.error);
|
||||
}
|
||||
|
||||
trackEvent({
|
||||
category: "teamOnboarding",
|
||||
action: "updateTeam",
|
||||
label: "success",
|
||||
});
|
||||
|
||||
return res.data;
|
||||
}}
|
||||
/>
|
||||
@@ -98,13 +89,13 @@ export function InviteTeamMembers(props: {
|
||||
client: ThirdwebClient;
|
||||
}) {
|
||||
const router = useDashboardRouter();
|
||||
const trackEvent = useTrack();
|
||||
|
||||
return (
|
||||
<InviteTeamMembersUI
|
||||
trackEvent={trackEvent}
|
||||
client={props.client}
|
||||
onComplete={() => {
|
||||
// at this point the team onboarding is complete
|
||||
reportOnboardingCompleted();
|
||||
router.replace(`/team/${props.team.slug}`);
|
||||
}}
|
||||
getTeam={async () => {
|
||||
@@ -128,27 +119,10 @@ export function InviteTeamMembers(props: {
|
||||
invites: params,
|
||||
});
|
||||
|
||||
trackEvent({
|
||||
category: "teamOnboarding",
|
||||
action: "inviteTeamMembers",
|
||||
label: "attempt",
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
trackEvent({
|
||||
category: "teamOnboarding",
|
||||
action: "inviteTeamMembers",
|
||||
label: "error",
|
||||
});
|
||||
throw new Error(res.errorMessage);
|
||||
}
|
||||
|
||||
trackEvent({
|
||||
category: "teamOnboarding",
|
||||
action: "inviteTeamMembers",
|
||||
label: "success",
|
||||
});
|
||||
|
||||
return {
|
||||
results: res.results,
|
||||
};
|
||||
|
||||
+4
-2
@@ -76,7 +76,7 @@ export function InviteSection(props: {
|
||||
inviteTeamMembers: InviteTeamMembersFn;
|
||||
customCTASection?: React.ReactNode;
|
||||
className?: string;
|
||||
onInviteSuccess?: () => void;
|
||||
onInviteSuccess?: (successCount: number) => void;
|
||||
shouldHideInviteButton?: boolean;
|
||||
recommendedMembers: RecommendedMember[];
|
||||
client: ThirdwebClient;
|
||||
@@ -219,7 +219,9 @@ export function InviteSection(props: {
|
||||
);
|
||||
|
||||
if (props.onInviteSuccess) {
|
||||
props.onInviteSuccess();
|
||||
props.onInviteSuccess(
|
||||
data.inviteStatuses.filter((r) => r === "fulfilled").length,
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
+14
-32
@@ -1,4 +1,8 @@
|
||||
"use client";
|
||||
import {
|
||||
reportContractDeployFailed,
|
||||
reportContractDeployed,
|
||||
} from "@/analytics/report";
|
||||
import {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
@@ -21,7 +25,6 @@ import {
|
||||
} from "constants/addresses";
|
||||
import { ZERO_FEE_CHAINS } from "constants/fee-config";
|
||||
import { SolidityInput } from "contract-ui/components/solidity-inputs";
|
||||
import { useTrack } from "hooks/analytics/useTrack";
|
||||
import { useTxNotifications } from "hooks/useTxNotifications";
|
||||
import { replaceTemplateValues } from "lib/deployment/template-values";
|
||||
import {
|
||||
@@ -50,6 +53,7 @@ import {
|
||||
LAST_USED_TEAM_ID,
|
||||
} from "../../../constants/cookies";
|
||||
import { getCookie } from "../../../lib/cookie";
|
||||
import { parseError } from "../../../utils/errorParser";
|
||||
import { useCustomFactoryAbi, useFunctionParamsFromABI } from "../hooks";
|
||||
import {
|
||||
AddToProjectCardUI,
|
||||
@@ -204,7 +208,6 @@ export const CustomContractForm: React.FC<CustomContractFormProps> = ({
|
||||
"Successfully deployed contract",
|
||||
"Failed to deploy contract",
|
||||
);
|
||||
const trackEvent = useTrack();
|
||||
|
||||
const constructorParams =
|
||||
metadata.abi.find((a) => a.type === "constructor")?.inputs || [];
|
||||
@@ -638,21 +641,6 @@ export const CustomContractForm: React.FC<CustomContractFormProps> = ({
|
||||
},
|
||||
];
|
||||
|
||||
const publisherAnalyticsData = metadata.publisher
|
||||
? {
|
||||
publisherAndContractName: `${rewriteTwPublisher(metadata.publisher)}/${metadata.name}`,
|
||||
}
|
||||
: {};
|
||||
|
||||
trackEvent({
|
||||
category: "custom-contract",
|
||||
action: "deploy",
|
||||
label: "attempt",
|
||||
...publisherAnalyticsData,
|
||||
chainId: walletChain.id,
|
||||
metadataUri: metadata.metadataUri,
|
||||
});
|
||||
|
||||
deployStatusModal.setViewContractLink("");
|
||||
deployStatusModal.open(steps);
|
||||
try {
|
||||
@@ -668,15 +656,13 @@ export const CustomContractForm: React.FC<CustomContractFormProps> = ({
|
||||
}),
|
||||
);
|
||||
|
||||
trackEvent({
|
||||
category: "custom-contract",
|
||||
action: "deploy",
|
||||
label: "success",
|
||||
...publisherAnalyticsData,
|
||||
contractAddress: contractAddr,
|
||||
reportContractDeployed({
|
||||
address: contractAddr,
|
||||
chainId: walletChain.id,
|
||||
metadataUri: metadata.metadataUri,
|
||||
publisher: rewriteTwPublisher(metadata.publisher),
|
||||
contractName: metadata.name,
|
||||
});
|
||||
|
||||
deployStatusModal.nextStep();
|
||||
if (importSelection.team && importSelection.project) {
|
||||
deployStatusModal.setViewContractLink(
|
||||
@@ -707,15 +693,11 @@ export const CustomContractForm: React.FC<CustomContractFormProps> = ({
|
||||
} catch (e) {
|
||||
onError(e);
|
||||
console.error("failed to deploy contract", e);
|
||||
trackEvent({
|
||||
category: "custom-contract",
|
||||
action: "deploy",
|
||||
label: "error",
|
||||
...publisherAnalyticsData,
|
||||
chainId: walletChain.id,
|
||||
metadataUri: metadata.metadataUri,
|
||||
error: e,
|
||||
const parsedError = parseError(e);
|
||||
reportContractDeployFailed({
|
||||
errorMessage: parsedError,
|
||||
});
|
||||
|
||||
deployStatusModal.close();
|
||||
}
|
||||
})}
|
||||
|
||||
@@ -30,7 +30,7 @@ export function parseErrorToMessage(error: unknown): string | JSX.Element {
|
||||
);
|
||||
}
|
||||
|
||||
export function parseError(error: unknown): string | JSX.Element {
|
||||
export function parseError(error: unknown): string {
|
||||
// if the error is a straight string just return it
|
||||
if (typeof error === "string") {
|
||||
return error;
|
||||
@@ -82,19 +82,11 @@ function isErrorWithCode(error: unknown): error is ErrorWithCode {
|
||||
return (error as ErrorWithCode)?.code !== undefined;
|
||||
}
|
||||
|
||||
function parseErrorCode(
|
||||
error: ErrorWithCode,
|
||||
): string | JSX.Element | undefined {
|
||||
function parseErrorCode(error: ErrorWithCode): string | undefined {
|
||||
switch (error.code) {
|
||||
case "CALL_EXCEPTION": {
|
||||
if (error.reason) {
|
||||
return (
|
||||
<>
|
||||
The following error happened on the underlying Smart Contract:
|
||||
<br />
|
||||
<strong>{error.reason}</strong>
|
||||
</>
|
||||
);
|
||||
return `The following error happened on the underlying Smart Contract: ${error.reason}`;
|
||||
}
|
||||
return "An error occurred on the underlying Smart Contract.";
|
||||
}
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
*.mdx
|
||||
*.md
|
||||
node_modules
|
||||
@@ -0,0 +1,67 @@
|
||||
# Analytics Guidelines
|
||||
|
||||
This folder centralises the **PostHog** tracking logic for the dashboard app.
|
||||
Most developers will only need to add or extend _event-reporting_ functions in `report.ts`.
|
||||
|
||||
---
|
||||
|
||||
## 1. When to add an event
|
||||
1. Ask yourself if the data will be **actionable**. Every event should have a clear product or business question it helps answer.
|
||||
2. Check if a similar event already exists in `report.ts`. Avoid duplicates.
|
||||
|
||||
---
|
||||
|
||||
## 2. Naming conventions
|
||||
| Concept | Convention | Example |
|
||||
|---------|------------|---------|
|
||||
| **Event name** (string sent to PostHog) | Human-readable phrase formatted as `<subject> <verb>` | `"contract deployed"` |
|
||||
| **Reporting function** | `report<Subject><Verb>` (PascalCase) | `reportContractDeployed` |
|
||||
| **File** | All event functions live in the shared `report.ts` file (for now) | — |
|
||||
|
||||
> Keeping names predictable makes it easy to search both code and analytics.
|
||||
|
||||
---
|
||||
|
||||
## 3. Boilerplate / template
|
||||
Add a new function to `report.ts` following this pattern:
|
||||
|
||||
```ts
|
||||
/**
|
||||
* ### Why do we need to report this event?
|
||||
* - _Add bullet points explaining the product metrics/questions this event answers._
|
||||
*
|
||||
* ### Who is responsible for this event?
|
||||
* @your-github-handle
|
||||
*/
|
||||
export function reportExampleEvent(properties: {
|
||||
/* Add typed properties here */
|
||||
}) {
|
||||
posthog.capture("example event", {
|
||||
/* Pass the same properties here */
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
Guidelines:
|
||||
1. **Explain the "why".** The JSDoc block is mandatory so future contributors know the purpose.
|
||||
2. **Type everything.** The `properties` object should be fully typed—this doubles as documentation.
|
||||
3. **Client-side only.** `posthog-js` must never run on the server. Call these reporting helpers from client components, event handlers, etc.
|
||||
|
||||
---
|
||||
|
||||
## 4. Editing or removing events
|
||||
1. Update both the function and the PostHog event definition (if required).
|
||||
2. Inform the core services team before removing or renaming an event.
|
||||
|
||||
---
|
||||
|
||||
## 5. Identification & housekeeping (FYI)
|
||||
Most devs can ignore this section, but for completeness:
|
||||
|
||||
- `hooks/identify-account.ts` and `hooks/identify-team.ts` wrap `posthog.identify`/`group` calls.
|
||||
- `resetAnalytics` clears identity state (used on logout).
|
||||
|
||||
---
|
||||
|
||||
## 6. Need help?
|
||||
Ping #eng-core-services in slack.
|
||||
@@ -0,0 +1,20 @@
|
||||
import posthog from "posthog-js";
|
||||
|
||||
// ----------------------------
|
||||
// FEEDBACK
|
||||
// ----------------------------
|
||||
|
||||
/**
|
||||
* ### Why do we need to report this event?
|
||||
* - To track the ratio of positive and negative feedback
|
||||
* - To record the feedback for future improvements of the documentation
|
||||
*
|
||||
* ### Who is responsible for this event?
|
||||
* @jnsdls
|
||||
*
|
||||
*/
|
||||
export function reportFeedback(
|
||||
properties: { helpful: true } | { helpful: false; feedback: string },
|
||||
) {
|
||||
posthog.capture("documentation feedback submitted", properties);
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { reportFeedback } from "@/analytics/report";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog";
|
||||
import { BadgeCheckIcon, ThumbsDownIcon, ThumbsUpIcon } from "lucide-react";
|
||||
@@ -20,6 +21,7 @@ export function Feedback() {
|
||||
className="gap-2 bg-card"
|
||||
onClick={() => {
|
||||
setIsSubmitted(true);
|
||||
reportFeedback({ helpful: true });
|
||||
}}
|
||||
>
|
||||
Yes
|
||||
@@ -54,6 +56,7 @@ export function Feedback() {
|
||||
<Button
|
||||
onClick={() => {
|
||||
setIsSubmitted(true);
|
||||
reportFeedback({ helpful: false, feedback });
|
||||
}}
|
||||
>
|
||||
Submit
|
||||
|
||||
Reference in New Issue
Block a user