diff --git a/.github/workflows/posthog-types.yml b/.github/workflows/posthog-types.yml new file mode 100644 index 00000000..da80a443 --- /dev/null +++ b/.github/workflows/posthog-types.yml @@ -0,0 +1,116 @@ +name: posthog-types + +on: + push: + branches: [master] + pull_request: + branches: [master] + +defaults: + run: + shell: bash + working-directory: . + +jobs: + build: + runs-on: ubicloud + timeout-minutes: 10 + defaults: + run: + working-directory: ./posthog-types + steps: + - uses: actions/checkout@v4 + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: '24.x.x' + registry-url: 'https://registry.npmjs.org' + + - name: Setup bun + uses: oven-sh/setup-bun@v1 + with: + bun-version: 1.2.15 + + - name: Install dependencies + run: bun install + + - name: Build + run: bun run build + + check-for-posthog-types-changes: + runs-on: ubicloud + outputs: + posthog-types: ${{ steps.filter.outputs.posthog-types }} + steps: + - uses: actions/checkout@v4 + - uses: dorny/paths-filter@v2 + id: filter + with: + filters: | + posthog-types: + - 'posthog-types/**' + + release: + runs-on: ubicloud + needs: [build, check-for-posthog-types-changes] + if: ${{ github.ref == 'refs/heads/master' && needs.check-for-posthog-types-changes.outputs.posthog-types == 'true' }} + defaults: + run: + working-directory: ./posthog-types + steps: + - uses: actions/checkout@v4 + with: + token: ${{ secrets.GH_PAT }} + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: '24.x.x' + registry-url: 'https://registry.npmjs.org' + + - name: Setup bun + uses: oven-sh/setup-bun@v1 + with: + bun-version: 1.2.15 + + - name: Install dependencies + run: bun install + + - name: Build + run: bun run build + + - name: Update package version + run: | + VERSION=$(node -e "console.log(require('./package.json').version);") + npm version patch + echo "PACKAGE_VERSION=$(node -e "console.log(require('./package.json').version);")" >> $GITHUB_ENV + + - name: Git commit + id: git-commit + run: | + git config user.name "GitHub Actions" + git config user.email 41898282+github-actions[bot]@users.noreply.github.com + git add .. + git commit -a -m "posthog-types: release v$PACKAGE_VERSION [skip ci]" + git pull --rebase origin master + git push origin HEAD || { + echo "Push failed. Retrying after pulling latest changes..." + git pull --rebase origin master + git push origin HEAD + } + echo "version=$PACKAGE_VERSION" >> $GITHUB_OUTPUT + + - name: Publish to npm + run: npm publish --access=public + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + + - name: Notify Slack on failure + if: failure() + uses: slackapi/slack-github-action@v1.24.0 + env: + SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }} + with: + channel-id: '#code-review' + slack-message: ' ❌ posthog-types deployment failed! Check the logs: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}' diff --git a/posthog-types/package.json b/posthog-types/package.json new file mode 100644 index 00000000..b823b6cc --- /dev/null +++ b/posthog-types/package.json @@ -0,0 +1,26 @@ +{ + "name": "@drift-labs/posthog-types", + "version": "0.0.1", + "description": "Shared PostHog event type definitions for Drift", + "types": "./lib/index.d.ts", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/drift-labs/drift-common/tree/master/posthog-types" + }, + "author": "Drift Labs", + "scripts": { + "build": "bun run clean && tsc", + "clean": "rm -rf lib", + "prepublishOnly": "bun run build" + }, + "files": [ + "lib" + ], + "publishConfig": { + "access": "public" + }, + "devDependencies": { + "typescript": "5.4.5" + } +} \ No newline at end of file diff --git a/posthog-types/src/eventMap.ts b/posthog-types/src/eventMap.ts new file mode 100644 index 00000000..259d19dc --- /dev/null +++ b/posthog-types/src/eventMap.ts @@ -0,0 +1,77 @@ +import type { CollateralEvents } from './events/collateral'; +import type { TradeEvents } from './events/trade'; +import type { VaultEvents } from './events/vault'; +import type { IfStakingEvents } from './events/ifStaking'; +import type { EarnEvents } from './events/earn'; +import type { AmplifyEvents } from './events/amplify'; +import type { OnboardingEvents } from './events/onboarding'; +import type { SurveyEvents } from './events/survey'; +import type { PnlEvents } from './events/pnl'; +import type { SystemEvents } from './events/system'; + +/** + * Marker type for events that require no properties. + * We use {} rather than Record because Record + * collapses optional fields to never under intersection with platform extensions. + */ +// eslint-disable-next-line @typescript-eslint/ban-types +export type NoProperties = {}; + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface PostHogEventMap + extends CollateralEvents, + TradeEvents, + VaultEvents, + IfStakingEvents, + EarnEvents, + AmplifyEvents, + OnboardingEvents, + SurveyEvents, + PnlEvents, + SystemEvents {} + +export type PostHogEvent = keyof PostHogEventMap; + +/** + * Extracts event names whose property type has no required keys (i.e., empty `{}`). + * These events can be captured without passing a properties argument. + */ +type EmptyEventsOf = { + [K in keyof TMap]: keyof TMap[K] extends never ? K : never; +}[keyof TMap]; + +/** + * Typed capture function. Overloads are ordered so that: + * 1. Events with no properties can be called with just the event name + * 2. Events with properties require the properties argument + * + * TypeScript resolves overloads top-to-bottom — the zero-argument overload + * must come first, otherwise it would never match. + */ +export type CaptureEvent = { + & string>(event: E): void; + (event: E, properties: TMap[E]): void; +}; + +/** + * Extend the shared event map with platform-specific overrides and new events. + * + * @typeParam TOverrides - Properties to intersect onto existing events (use optional fields) + * @typeParam TNew - Entirely new platform-specific events + * + * @example + * ```ts + * type WebEventMap = ExtendEventMap< + * { collateral_deposit_submitted: { source?: string } }, + * { web_only_event: { detail: string } } + * >; + * ``` + */ +export type ExtendEventMap< + TOverrides extends Partial> = NoProperties, + TNew extends Record = NoProperties +> = { + [K in PostHogEvent]: K extends keyof TOverrides + ? PostHogEventMap[K] & TOverrides[K] + : PostHogEventMap[K]; +} & TNew; diff --git a/posthog-types/src/events/amplify.ts b/posthog-types/src/events/amplify.ts new file mode 100644 index 00000000..07702e21 --- /dev/null +++ b/posthog-types/src/events/amplify.ts @@ -0,0 +1,13 @@ +type AmplifyDepositProperties = { + amplify_deposit_amount: number; + amplify_deposit_leverage: number; + amplify_deposit_asset: string; + amplify_deposit_pair: string; +}; + +export type AmplifyEvents = { + amplify_deposit_cta_clicked: AmplifyDepositProperties; + amplify_deposit_succeeded: AmplifyDepositProperties & { + amplify_last_withdrawal_quote_30_mins: number; + }; +}; diff --git a/posthog-types/src/events/collateral.ts b/posthog-types/src/events/collateral.ts new file mode 100644 index 00000000..77e6787e --- /dev/null +++ b/posthog-types/src/events/collateral.ts @@ -0,0 +1,30 @@ +import type { NoProperties } from '../eventMap'; + +export type CollateralEvents = { + collateral_deposit_submitted: { + spot_market_symbol?: string; + newAccount: boolean; + depositAmount: number; + }; + collateral_withdrawal_submitted: { + spot_market_symbol: string; + withdrawal_amount: string; + }; + collateral_borrow_submitted: { + spot_market_symbol: string; + borrow_amount: string; + }; + collateral_transfer_submitted: NoProperties; + collateral_deposit_modal_opened: { + cta: string; + from: string; + }; + collateral_funxyz_address_copied: { + from_chain_id: number | string | null; + from_chain_name?: string | null; + from_asset_symbol: string | null; + to_chain: string; + to_asset_symbol: string; + receiving_address: string; + }; +}; diff --git a/posthog-types/src/events/earn.ts b/posthog-types/src/events/earn.ts new file mode 100644 index 00000000..d37e995e --- /dev/null +++ b/posthog-types/src/events/earn.ts @@ -0,0 +1,8 @@ +import type { NoProperties } from '../eventMap'; + +export type EarnEvents = { + earn_if_stake_clicked: NoProperties; + earn_vaults_clicked: NoProperties; + earn_dsol_staking_clicked: NoProperties; + earn_amplify_clicked: NoProperties; +}; diff --git a/posthog-types/src/events/ifStaking.ts b/posthog-types/src/events/ifStaking.ts new file mode 100644 index 00000000..fbadc489 --- /dev/null +++ b/posthog-types/src/events/ifStaking.ts @@ -0,0 +1,14 @@ +export type IfStakingEvents = { + if_staking_stake_submitted: { + spot_market_symbol: string; + }; + if_staking_unstake_submitted: { + spot_market_symbol: string; + }; + if_staking_unstake_canceled: { + spot_market_symbol: string; + }; + if_staking_unstaked_assets_withdrawn: { + spot_market_symbol: string; + }; +}; diff --git a/posthog-types/src/events/index.ts b/posthog-types/src/events/index.ts new file mode 100644 index 00000000..f25f8cdc --- /dev/null +++ b/posthog-types/src/events/index.ts @@ -0,0 +1,10 @@ +export type { CollateralEvents } from './collateral'; +export type { TradeEvents } from './trade'; +export type { VaultEvents } from './vault'; +export type { IfStakingEvents } from './ifStaking'; +export type { EarnEvents } from './earn'; +export type { AmplifyEvents } from './amplify'; +export type { OnboardingEvents } from './onboarding'; +export type { SurveyEvents } from './survey'; +export type { PnlEvents } from './pnl'; +export type { SystemEvents } from './system'; diff --git a/posthog-types/src/events/onboarding.ts b/posthog-types/src/events/onboarding.ts new file mode 100644 index 00000000..bdb80150 --- /dev/null +++ b/posthog-types/src/events/onboarding.ts @@ -0,0 +1,40 @@ +import type { NoProperties } from '../eventMap'; + +type PrivyErrorProperties = { + error_message: string; + error_stack?: string; + error_name?: string; + error_raw: string; + env: string; + wallet_address: string; + use_fee_payer: boolean; +}; + +export type OnboardingEvents = { + onboarding_wallet_connected: { + name: string; + }; + onboarding_magic_auth_login: { + auth_type: 'email' | 'other'; + success: boolean; + }; + onboarding_subaccount_created: NoProperties; + onboarding_signless_modal_opened: NoProperties; + onboarding_signless_delegated_confirmed: { + success: boolean; + delegated_selected_option: string; + }; + onboarding_signless_setup_succeeded: { + is_ledger: boolean; + }; + onboarding_wallet_connection_error: { + provider: string; + error: unknown; + }; + onboarding_referral_creation_error: { + error: unknown; + }; + onboarding_privy_sign_send_error: PrivyErrorProperties; + onboarding_privy_build_sign_error: PrivyErrorProperties; + onboarding_account_deleted: NoProperties; +}; diff --git a/posthog-types/src/events/pnl.ts b/posthog-types/src/events/pnl.ts new file mode 100644 index 00000000..717c080f --- /dev/null +++ b/posthog-types/src/events/pnl.ts @@ -0,0 +1,10 @@ +export type PnlEvents = { + pnl_action_performed: { + action: 'download' | 'copy' | 'share_on_x'; + }; + pnl_history_exported: { + statement_type: string; + date_from: string; + date_to: string; + }; +}; diff --git a/posthog-types/src/events/survey.ts b/posthog-types/src/events/survey.ts new file mode 100644 index 00000000..23768596 --- /dev/null +++ b/posthog-types/src/events/survey.ts @@ -0,0 +1,18 @@ +/** + * Survey events use PostHog-reserved names with spaces (not snake_case). + * These exact strings are required for PostHog's Surveys UI, response rate + * tracking, and analytics to work. Do NOT rename them. + * See: https://posthog.com/docs/surveys/implementing-custom-surveys + */ +export type SurveyEvents = { + 'survey shown': { + $survey_id: string; + }; + 'survey dismissed': { + $survey_id: string; + }; + 'survey sent': { + $survey_id: string; + [key: string]: unknown; + }; +}; diff --git a/posthog-types/src/events/system.ts b/posthog-types/src/events/system.ts new file mode 100644 index 00000000..d16ff7a9 --- /dev/null +++ b/posthog-types/src/events/system.ts @@ -0,0 +1,49 @@ +export type SystemEvents = { + system_unmatched_error: { + error_message?: string; + error_stack?: string; + error_raw: unknown; + wallet?: string; + }; + system_transaction_error: { + errorClass?: string; + errorId?: string; + }; + system_unhandled_error: { + error_message: unknown; + source?: string; + lineno?: number; + colno?: number; + stack?: string; + url: string; + timestamp: string; + authority?: string; + }; + system_unhandled_promise_rejection: { + error_message: string; + stack?: string; + url: string; + timestamp: string; + authority?: string; + }; + system_performance_snapshot: { + requestsPerMinute?: number; + maxSlotGapMs?: number; + maxSlotGapCount?: number; + totalJSHeapSize?: number; + usedJSHeapSize?: number; + eventLoopLag?: number[]; + dlobSource?: { + source: string; + marketId: string; + }; + primarySubscriptionMethod?: string; + metricsId?: string; + historyServerRequestsTarget?: unknown; + historyServerRequestsSource?: unknown; + }; + system_startup_time: { + metricKey: string; + timeMs: number; + }; +}; diff --git a/posthog-types/src/events/trade.ts b/posthog-types/src/events/trade.ts new file mode 100644 index 00000000..d72ac20c --- /dev/null +++ b/posthog-types/src/events/trade.ts @@ -0,0 +1,102 @@ +import type { NoProperties } from '../eventMap'; + +type OrderTimingProperties = { + tx_signature?: string; + order_id: number; + user_account_key?: string; + order_method: string; + order_params: unknown; + auction_version?: 1 | 2; + hash?: string; + oracle_price?: string; +}; + +export type TradeEvents = { + trade_placed: { + market_type: 'perps' | 'spot'; + order_type: string; + direction?: string; + market_symbol?: string; + trade_base: number; + trade_notional: number; + geolocation_history?: unknown; + is_new_account?: boolean; + next_order_id?: number; + is_devmode?: boolean; + is_successful?: boolean; + swift_uuid?: string; + }; + trade_orderbook_crossed: { + debugInfo: unknown; + }; + trade_order_params_requested: { + user_account_pubkey: unknown; + url_params: Record; + oracle_price: unknown; + mm_oracle_price: unknown; + regular_oracle_price: unknown; + derived_market_state?: { + mark_price: string; + best_bid: string; + best_ask: string; + update_slot: number; + }; + endpointResponse: unknown; + }; + trade_order_fill_time_recorded: { + time_to_fill_ms: number; + } & OrderTimingProperties; + trade_order_confirmation_time_recorded: { + time_to_confirm_ms: number; + } & OrderTimingProperties; + trade_perp_non_market_order_timed: { + ix_creation_time_ms: number | null; + tx_building_time_ms: number | null; + fee_payer_signing_time_ms: number | null; + embedded_wallet_signing_time_ms: number | null; + send_transaction_time_ms: number | null; + overall_time_ms: number; + market_index: number; + order_type: string; + base_amount: number; + }; + trade_position_closed: NoProperties; + trade_favorite_market_added: { + market_symbol: string; + }; + trade_favorite_market_removed: { + market_symbol: string; + }; + trade_swift_error: { + error: unknown; + [key: string]: unknown; + }; + trade_market_order_error: { + message: string; + }; + trade_oracle_mark_divergence: { + orderType?: unknown; + market: unknown; + markPrice: number; + oraclePrice: number; + absPriceDiff: number; + subscriptionState: unknown; + topBidInMarketStateStore: number; + topAskInMarketStateStore: number; + markPriceInMarketStateStore: number; + oraclePriceInMarketStateStore: number; + userSettings: unknown; + swiftEnabled?: boolean; + }; + trade_close_multiple_positions_batched: { + total_positions: number; + batch_count: number; + batch_sizes: number[]; + is_place_and_take: boolean; + market_indexes: number[]; + }; + trade_close_multiple_positions_size_error: { + market_indexes: number[]; + error_message: string; + }; +}; diff --git a/posthog-types/src/events/vault.ts b/posthog-types/src/events/vault.ts new file mode 100644 index 00000000..04d5eb03 --- /dev/null +++ b/posthog-types/src/events/vault.ts @@ -0,0 +1,14 @@ +import type { NoProperties } from '../eventMap'; + +export type VaultEvents = { + vault_deposit_submitted: NoProperties; + vault_withdrawal_submitted: { + amount: number; + }; + vault_viewed: NoProperties; + vault_overview_viewed: NoProperties; + vault_side_panel_opened: NoProperties; + vault_detail_opened: NoProperties; + vault_inspected: NoProperties; + vault_eco_page_viewed: NoProperties; +}; diff --git a/posthog-types/src/index.ts b/posthog-types/src/index.ts new file mode 100644 index 00000000..bb0344cb --- /dev/null +++ b/posthog-types/src/index.ts @@ -0,0 +1,25 @@ +export type { + PostHogEventMap, + PostHogEvent, + NoProperties, + CaptureEvent, + ExtendEventMap, +} from './eventMap'; + +export type { + StaticSuperProperties, + DynamicSuperProperties, +} from './superProperties'; + +export type { + CollateralEvents, + TradeEvents, + VaultEvents, + IfStakingEvents, + EarnEvents, + AmplifyEvents, + OnboardingEvents, + SurveyEvents, + PnlEvents, + SystemEvents, +} from './events'; diff --git a/posthog-types/src/superProperties.ts b/posthog-types/src/superProperties.ts new file mode 100644 index 00000000..b0c1985a --- /dev/null +++ b/posthog-types/src/superProperties.ts @@ -0,0 +1,13 @@ +/** Static super properties — set once at PostHog init via posthog.register() */ +export type StaticSuperProperties = { + platform: 'web' | 'ios' | 'android'; + env: string; +}; + +/** Dynamic super properties — updated on wallet state changes via posthog.register() */ +export type DynamicSuperProperties = { + connected: boolean; + account_exists: boolean | null; + authority: string | null; + wallet_name: string | null; +}; diff --git a/posthog-types/tsconfig.json b/posthog-types/tsconfig.json new file mode 100644 index 00000000..c5502c23 --- /dev/null +++ b/posthog-types/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "ES2019", + "declaration": true, + "emitDeclarationOnly": true, + "module": "commonjs", + "esModuleInterop": true, + "strict": true, + "skipLibCheck": true, + "baseUrl": ".", + "moduleResolution": "node", + "outDir": "./lib" + }, + "include": ["src/**/*"], + "exclude": ["node_modules"] +}