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:
jnsdls
2025-06-19 00:53:58 +00:00
parent d9eee50dc1
commit 2aa9ce5f78
16 changed files with 435 additions and 161 deletions
+26 -1
View File
@@ -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);
}
```
+35
View File
@@ -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
+67
View File
@@ -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.
+150
View File
@@ -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");
}
@@ -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);
@@ -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);
}}
@@ -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,
};
@@ -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,
);
}
}
},
@@ -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();
}
})}
+3 -11
View File
@@ -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
View File
@@ -1,2 +1,3 @@
*.mdx
*.md
node_modules
+67
View File
@@ -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.
+20
View File
@@ -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