@runhuman/sensor 0.2.2 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +129 -4
- package/dist/index.d.ts +129 -4
- package/dist/index.js +273 -2
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +270 -1
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -7
- package/dist/overlay/index.d.mts +0 -30
- package/dist/overlay/index.d.ts +0 -30
- package/dist/overlay/index.js +0 -1431
- package/dist/overlay/index.js.map +0 -1
- package/dist/overlay/index.mjs +0 -1410
- package/dist/overlay/index.mjs.map +0 -1
- package/dist/session-manager-B6tiwEQm.d.mts +0 -99
- package/dist/session-manager-B6tiwEQm.d.ts +0 -99
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/overlay/RunhumanOverlay.tsx","../../src/overlay/use-sensor-state.ts","../../../shared/src/routes/route-builder.ts","../../../shared/src/routes/web-routes.ts","../../../shared/src/routes/api-routes.ts","../../src/api-client.ts","../../src/event-buffer.ts","../../src/interceptors/network.interceptor.ts","../../src/interceptors/console.interceptor.ts","../../src/interceptors/error.interceptor.ts","../../src/interceptors/index.ts","../../src/session-manager.ts","../../src/deep-link-handler.ts","../../src/runhuman.ts","../../src/overlay/CodeEntryPanel.tsx","../../src/overlay/overlay-styles.ts","../../src/overlay/ActiveIndicator.tsx"],"sourcesContent":["/**\r\n * RunhumanOverlay — Wrap your app to enable sensor activation UI.\r\n *\r\n * Usage:\r\n * import { RunhumanOverlay } from '@runhuman/sensor/overlay';\r\n *\r\n * <RunhumanOverlay>\r\n * <App />\r\n * </RunhumanOverlay>\r\n *\r\n * Shows a code entry panel when idle (no jobId), and a small indicator\r\n * dot when the sensor is active, polling, or ending.\r\n */\r\n\r\nimport React from 'react';\r\nimport { View, StyleSheet } from 'react-native';\r\nimport { useSensorState } from './use-sensor-state.js';\r\nimport { CodeEntryPanel } from './CodeEntryPanel.js';\r\nimport { ActiveIndicator } from './ActiveIndicator.js';\r\n\r\nexport type OverlayPosition = 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';\r\n\r\nexport interface RunhumanOverlayProps {\r\n children: React.ReactNode;\r\n /** Corner for the overlay UI (default: 'bottom-right') */\r\n position?: OverlayPosition;\r\n}\r\n\r\nconst positionMap = {\r\n 'top-left': 'topLeft',\r\n 'top-right': 'topRight',\r\n 'bottom-left': 'bottomLeft',\r\n 'bottom-right': 'bottomRight',\r\n} as const;\r\n\r\nexport function RunhumanOverlay({ children, position = 'bottom-right' }: RunhumanOverlayProps) {\r\n const { state, activeJobId } = useSensorState();\r\n const posKey = positionMap[position];\r\n\r\n return (\r\n <View style={styles.container}>\r\n {children}\r\n {state === 'idle' && !activeJobId ? (\r\n <CodeEntryPanel position={posKey} />\r\n ) : null}\r\n {state !== 'idle' ? (\r\n <ActiveIndicator state={state} position={posKey} />\r\n ) : null}\r\n </View>\r\n );\r\n}\r\n\r\nconst styles = StyleSheet.create({\r\n container: {\r\n flex: 1,\r\n },\r\n});\r\n","/**\r\n * React hook for subscribing to sensor state changes.\r\n * Uses useSyncExternalStore for tear-safe subscriptions.\r\n */\r\n\r\nimport { useSyncExternalStore } from 'react';\r\nimport { Runhuman } from '../runhuman.js';\r\nimport type { SessionState } from '../session-manager.js';\r\n\r\nexport interface SensorState {\r\n state: SessionState;\r\n activeJobId: string | null;\r\n sessionId: string | null;\r\n}\r\n\r\nfunction subscribe(onStoreChange: () => void): () => void {\r\n const sm = Runhuman.getInstance().getSessionManager();\r\n return sm.subscribe(onStoreChange);\r\n}\r\n\r\nfunction getSnapshot(): SensorState {\r\n const sm = Runhuman.getInstance().getSessionManager();\r\n return {\r\n state: sm.getSnapshot(),\r\n activeJobId: sm.getActiveJobId(),\r\n sessionId: sm.getSessionId(),\r\n };\r\n}\r\n\r\n/**\r\n * Subscribe to the sensor's session state.\r\n * Must be called after Runhuman.init().\r\n */\r\nexport function useSensorState(): SensorState {\r\n return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);\r\n}\r\n","/**\r\n * Type-safe route builder system\r\n *\r\n * Provides compile-time type checking for route parameters.\r\n * Usage:\r\n * const route = defineRoute('/dashboard/:projectId/jobs/:jobId?');\r\n * route.build({ projectId: 'abc' }); // '/dashboard/abc/jobs'\r\n * route.build({ projectId: 'abc', jobId: '1' }); // '/dashboard/abc/jobs/1'\r\n */\r\n\r\n/**\r\n * API prefix for backend routes.\r\n * Frontend apiClient adds this prefix automatically.\r\n * Backend should use withApiPrefix() to add this to route patterns.\r\n */\r\nexport const API_PREFIX = '/api' as const;\r\n\r\n/**\r\n * Prepends the API prefix to a route pattern.\r\n * Used by backend to ensure routes match what frontend expects.\r\n *\r\n * @example\r\n * withApiPrefix('/organizations') // => '/api/organizations'\r\n * withApiPrefix(apiRoutes.organizations.pattern) // => '/api/organizations'\r\n */\r\nexport function withApiPrefix<T extends string>(pattern: T): `/api${T}` {\r\n return `${API_PREFIX}${pattern}` as `/api${T}`;\r\n}\r\n\r\n// Helper type to simplify intersected object types into a single object type\r\ntype Simplify<T> = { [K in keyof T]: T[K] } & {};\r\n\r\n/**\r\n * Extracts route parameters from a route pattern string.\r\n * - `:param` becomes required: { param: string }\r\n * - `:param?` becomes optional: { param?: string }\r\n *\r\n * Examples:\r\n * - '/dashboard/:projectId' → { projectId: string }\r\n * - '/dashboard/:projectId/jobs/:jobId?' → { projectId: string; jobId?: string }\r\n * - '/dashboard' → {}\r\n */\r\nexport type ExtractRouteParams<T extends string> =\r\n // Optional param followed by more path: :param?/rest\r\n T extends `${string}:${infer Param}?/${infer Rest}`\r\n ? Simplify<{ [K in Param]?: string } & ExtractRouteParams<`/${Rest}`>>\r\n : // Required param followed by more path: :param/rest\r\n T extends `${string}:${infer Param}/${infer Rest}`\r\n ? Simplify<{ [K in Param]: string } & ExtractRouteParams<`/${Rest}`>>\r\n : // Optional param at end: :param?\r\n T extends `${string}:${infer Param}?`\r\n ? { [K in Param]?: string }\r\n : // Required param at end: :param\r\n T extends `${string}:${infer Param}`\r\n ? { [K in Param]: string }\r\n : // No params\r\n // eslint-disable-next-line @typescript-eslint/no-empty-object-type\r\n {};\r\n\r\n/**\r\n * Check if a type is an empty object (no params)\r\n */\r\ntype IsEmptyObject<T> = keyof T extends never ? true : false;\r\n\r\n/**\r\n * Route object with pattern and type-safe build function\r\n */\r\nexport interface Route<TPattern extends string> {\r\n /** The raw route pattern with :param placeholders */\r\n pattern: TPattern;\r\n\r\n /**\r\n * Build a URL from the route pattern and params.\r\n * - If route has no params: build() takes no arguments\r\n * - If route has params: build(params) requires matching params object\r\n */\r\n build: IsEmptyObject<ExtractRouteParams<TPattern>> extends true\r\n ? () => string\r\n : (params: ExtractRouteParams<TPattern>) => string;\r\n}\r\n\r\n/**\r\n * Creates a type-safe route builder.\r\n *\r\n * @param pattern Route pattern with :param placeholders (e.g., '/dashboard/:projectId/jobs')\r\n * @returns Route object with pattern and build function\r\n *\r\n * @example\r\n * const route = defineRoute('/dashboard/:projectId/jobs/:jobId?');\r\n * route.pattern; // '/dashboard/:projectId/jobs/:jobId?'\r\n * route.build({ projectId: 'abc' }); // '/dashboard/abc/jobs'\r\n * route.build({ projectId: 'abc', jobId: '123' }); // '/dashboard/abc/jobs/123'\r\n */\r\nexport function defineRoute<TPattern extends string>(\r\n pattern: TPattern\r\n): Route<TPattern> {\r\n const build = (params?: Record<string, string | undefined>): string => {\r\n if (!params) return pattern;\r\n\r\n let result = pattern as string;\r\n\r\n // Replace all :param and :param? placeholders\r\n for (const [key, value] of Object.entries(params)) {\r\n if (value !== undefined) {\r\n // Replace both :param and :param? variants\r\n result = result.replace(`:${key}?`, value);\r\n result = result.replace(`:${key}`, value);\r\n }\r\n }\r\n\r\n // Remove any remaining optional params that weren't provided\r\n result = result.replace(/\\/:[^/]+\\?/g, '');\r\n\r\n // Clean up any double slashes that might result from removal\r\n result = result.replace(/\\/+/g, '/');\r\n\r\n // Remove trailing slash unless it's the root\r\n if (result.length > 1 && result.endsWith('/')) {\r\n result = result.slice(0, -1);\r\n }\r\n\r\n return result;\r\n };\r\n\r\n return {\r\n pattern,\r\n build: build as Route<TPattern>['build'],\r\n };\r\n}\r\n","/**\r\n * Frontend route definitions for the web dashboard\r\n */\r\nimport { defineRoute } from './route-builder.js';\r\n\r\n/**\r\n * All frontend routes for the web application.\r\n *\r\n * Usage:\r\n * import { webRoutes } from '@runhuman/shared';\r\n * navigate(webRoutes.playground.build({ projectId: 'abc' }));\r\n * // => '/dashboard/abc/playground'\r\n */\r\nexport const webRoutes = {\r\n // ============================================\r\n // Non-project routes (no projectId required)\r\n // ============================================\r\n\r\n /** Dashboard home page */\r\n dashboard: defineRoute('/dashboard'),\r\n\r\n /** Subscription management page */\r\n managePlan: defineRoute('/dashboard/manage-plan'),\r\n\r\n /** Plan selection / upgrade page */\r\n plans: defineRoute('/dashboard/plans'),\r\n\r\n /** Simple job view (redirects to full URL with projectId) */\r\n jobSimple: defineRoute('/dashboard/jobs/:jobId'),\r\n\r\n // ============================================\r\n // Admin routes (@volter.ai only)\r\n // ============================================\r\n\r\n /** Admin panel */\r\n admin: defineRoute('/dashboard/admin'),\r\n\r\n // ============================================\r\n // Organizations routes\r\n // ============================================\r\n\r\n /** Organizations list page */\r\n organizations: defineRoute('/dashboard/organizations'),\r\n\r\n /** Organization dashboard */\r\n organization: defineRoute('/dashboard/organizations/:organizationId'),\r\n\r\n /** Organization members page */\r\n organizationMembers: defineRoute('/dashboard/organizations/:organizationId/members'),\r\n\r\n /** Organization settings page */\r\n organizationSettings: defineRoute('/dashboard/organizations/:organizationId/settings'),\r\n\r\n /** Organization usage/billing page */\r\n organizationUsage: defineRoute('/dashboard/organizations/:organizationId/usage'),\r\n\r\n /** Organization projects page */\r\n organizationProjects: defineRoute('/dashboard/organizations/:organizationId/projects'),\r\n\r\n /** Organization jobs page (jobs across all projects) */\r\n organizationJobs: defineRoute('/dashboard/organizations/:organizationId/jobs'),\r\n\r\n /** Organization API keys page */\r\n organizationApiKeys: defineRoute('/dashboard/organizations/:organizationId/api-keys'),\r\n\r\n /** Organization integrations page */\r\n organizationIntegrations: defineRoute('/dashboard/organizations/:organizationId/integrations'),\r\n\r\n /** Organization GitHub integrations page */\r\n organizationGitHub: defineRoute('/dashboard/organizations/:organizationId/github'),\r\n\r\n /** Organization SSO/SAML configuration page */\r\n organizationSso: defineRoute('/dashboard/organizations/:organizationId/sso'),\r\n\r\n /** Organization schedules overview */\r\n organizationSchedules: defineRoute('/dashboard/organizations/:organizationId/schedules'),\r\n\r\n /** Import from GitHub page */\r\n organizationImportGitHub: defineRoute('/dashboard/organizations/:organizationId/import'),\r\n\r\n // ============================================\r\n // User settings routes\r\n // ============================================\r\n\r\n /** Settings root (redirects to account) */\r\n settings: defineRoute('/dashboard/settings'),\r\n\r\n /** Account settings page */\r\n settingsAccount: defineRoute('/dashboard/settings/account'),\r\n\r\n // ============================================\r\n // Tester portal routes\r\n // ============================================\r\n\r\n /** Tester dashboard/portal home */\r\n tester: defineRoute('/tester'),\r\n\r\n /** Tester available jobs list */\r\n testerJobs: defineRoute('/tester/jobs'),\r\n\r\n /** Tester settings page */\r\n testerSettings: defineRoute('/tester/settings'),\r\n\r\n /** Tester active test session */\r\n testerTest: defineRoute('/tester/test/:jobId'),\r\n\r\n // ============================================\r\n // Project routes (require projectId)\r\n // ============================================\r\n\r\n /** Project root (redirects to playground) */\r\n project: defineRoute('/dashboard/:projectId'),\r\n\r\n /** Playground page for testing */\r\n playground: defineRoute('/dashboard/:projectId/playground'),\r\n\r\n /** Jobs list page */\r\n jobs: defineRoute('/dashboard/:projectId/jobs'),\r\n\r\n /** Single job detail page */\r\n job: defineRoute('/dashboard/:projectId/jobs/:jobId'),\r\n\r\n /** Templates page */\r\n templates: defineRoute('/dashboard/:projectId/templates'),\r\n\r\n /** Single template detail (modal) */\r\n template: defineRoute('/dashboard/:projectId/templates/:templateId'),\r\n\r\n /** GitHub issues page */\r\n issues: defineRoute('/dashboard/:projectId/issues'),\r\n\r\n /** Single issue detail (modal) */\r\n issue: defineRoute('/dashboard/:projectId/issues/:issueNumber'),\r\n\r\n /** Issue test sessions page */\r\n issueSessions: defineRoute('/dashboard/:projectId/issue-sessions'),\r\n\r\n /** Single test session detail (modal) */\r\n issueSession: defineRoute('/dashboard/:projectId/issue-sessions/:sessionId'),\r\n\r\n /** Project settings page */\r\n projectSettings: defineRoute('/dashboard/:projectId/settings'),\r\n\r\n /** Schedules page */\r\n schedules: defineRoute('/dashboard/:projectId/schedules'),\r\n\r\n /** Flow charts page */\r\n flowCharts: defineRoute('/dashboard/:projectId/flowcharts'),\r\n\r\n /** Single flow chart detail (embedded in dashboard - deprecated, use flowChartView) */\r\n flowChart: defineRoute('/dashboard/:projectId/flowcharts/:flowchartId'),\r\n\r\n /** Full-page flow chart viewer (outside dashboard layout) */\r\n flowChartView: defineRoute('/view/flowchart/:projectId/:flowchartId'),\r\n\r\n // ============================================\r\n // External/public routes\r\n // ============================================\r\n\r\n /** Organization invite acceptance page */\r\n invite: defineRoute('/invite/:token'),\r\n\r\n /** Quick start flow for new users (pre-auth) */\r\n quickStart: defineRoute('/start'),\r\n\r\n /** Public job view with token-secured access */\r\n publicJob: defineRoute('/j/:jobId/:token'),\r\n\r\n /** Public system status page */\r\n statuspage: defineRoute('/statuspage'),\r\n\r\n /** Documentation */\r\n docs: defineRoute('/docs/setup'),\r\n\r\n /** Pricing page */\r\n pricing: defineRoute('/pricing'),\r\n\r\n /** Home page */\r\n home: defineRoute('/'),\r\n\r\n // ============================================\r\n // Learn Platform routes\r\n // ============================================\r\n\r\n /** Learn catalogue (default product) */\r\n learn: defineRoute('/learn'),\r\n\r\n /** Single lesson view */\r\n learnLesson: defineRoute('/learn/:lessonSlug'),\r\n} as const;\r\n\r\n/**\r\n * Type for all web route names\r\n */\r\nexport type WebRouteName = keyof typeof webRoutes;\r\n","/**\r\n * API endpoint definitions\r\n *\r\n * These routes are used by the web frontend to call the API.\r\n * Note: These are relative paths (no /api prefix for the client,\r\n * which adds /api automatically via API_BASE).\r\n */\r\nimport { defineRoute } from './route-builder.js';\r\n\r\n/**\r\n * All API endpoints.\r\n *\r\n * Usage:\r\n * import { apiRoutes } from '@runhuman/shared';\r\n * apiClient(apiRoutes.jobs.build());\r\n * // => '/jobs'\r\n */\r\nexport const apiRoutes = {\r\n // ============================================\r\n // Jobs endpoints\r\n // ============================================\r\n\r\n /** List jobs (GET) or create job (POST) */\r\n jobs: defineRoute('/jobs'),\r\n\r\n /** Get/update/delete specific job */\r\n job: defineRoute('/jobs/:jobId'),\r\n\r\n /** Get job status (for polling) */\r\n jobStatus: defineRoute('/jobs/:jobId/status'),\r\n\r\n /** Get individual job artifact by type */\r\n jobArtifact: defineRoute('/jobs/:jobId/artifacts/:artifactType'),\r\n\r\n /** Update job feedback and rating */\r\n jobFeedback: defineRoute('/jobs/:jobId/feedback'),\r\n\r\n /** Enable/disable public sharing for a job (POST = enable, DELETE = disable) */\r\n jobShare: defineRoute('/jobs/:jobId/share'),\r\n\r\n /** Claim a job (for testers) */\r\n jobClaim: defineRoute('/jobs/:jobId/claim'),\r\n\r\n /** Synchronous job execution */\r\n run: defineRoute('/run'),\r\n\r\n /** Get public job (no auth required, token-secured) */\r\n publicJob: defineRoute('/public/jobs/:jobId/:token'),\r\n\r\n // ============================================\r\n // Tester App Job endpoints (secured by testerToken)\r\n // ============================================\r\n\r\n /** Get/update job for tester app */\r\n testerJob: defineRoute('/tester/jobs/:jobId'),\r\n\r\n /** Get presigned upload URLs for artifacts */\r\n testerJobUploadUrls: defineRoute('/tester/jobs/:jobId/upload-urls'),\r\n\r\n /** Process test results with AI */\r\n testerJobProcessResults: defineRoute('/tester/jobs/:jobId/process-results'),\r\n\r\n /** Get processing status */\r\n testerJobProcessingStatus: defineRoute('/tester/jobs/:jobId/processing/:processingJobId'),\r\n\r\n /** Extract frames from video */\r\n testerJobExtractFrames: defineRoute('/tester/jobs/:jobId/extract-frames'),\r\n\r\n /** Abort job due to platform issue */\r\n testerJobAbort: defineRoute('/tester/jobs/:jobId/abort'),\r\n\r\n /** Mark job as invalid due to bad instructions */\r\n testerJobInvalid: defineRoute('/tester/jobs/:jobId/invalid'),\r\n\r\n /** Notify phase transition from tester app */\r\n testerJobPhase: defineRoute('/tester/jobs/:jobId/phase'),\r\n\r\n // ============================================\r\n // LiveKit endpoints (screen sharing & recording)\r\n // ============================================\r\n\r\n /** Start LiveKit session + get tester token (testerToken auth) */\r\n livekitTesterToken: defineRoute('/tester/jobs/:jobId/livekit-token'),\r\n\r\n /** End LiveKit session + stop egress (testerToken auth) */\r\n livekitEndSession: defineRoute('/tester/jobs/:jobId/livekit-end'),\r\n\r\n /** Save transcript entries from Web Speech API (testerToken auth) */\r\n livekitTranscript: defineRoute('/tester/jobs/:jobId/livekit-transcript'),\r\n\r\n /** Get viewer token for live watching (Clerk JWT auth) */\r\n livekitViewerToken: defineRoute('/jobs/:jobId/livekit-viewer-token'),\r\n\r\n /** Check if LiveKit session is active (Clerk JWT or testerToken auth) */\r\n livekitStatus: defineRoute('/jobs/:jobId/livekit-status'),\r\n\r\n /** Get presigned recording URL (Clerk JWT auth) */\r\n livekitRecording: defineRoute('/jobs/:jobId/livekit-recording'),\r\n\r\n // ============================================\r\n // API Keys endpoints\r\n // ============================================\r\n\r\n /** List API keys (GET) or create key (POST) */\r\n keys: defineRoute('/keys'),\r\n\r\n /** Get/update/delete specific key */\r\n key: defineRoute('/keys/:keyId'),\r\n\r\n /** Revoke an API key */\r\n keyRevoke: defineRoute('/keys/:keyId/revoke'),\r\n\r\n /** Get API key info from header (for authenticated requests) */\r\n keyInfo: defineRoute('/key-info'),\r\n\r\n // ============================================\r\n // Personal Access Tokens (PATs) endpoints\r\n // ============================================\r\n\r\n /** List PATs (GET) or create PAT (POST) */\r\n pats: defineRoute('/pats'),\r\n\r\n /** Get/delete specific PAT */\r\n pat: defineRoute('/pats/:patId'),\r\n\r\n /** Revoke a PAT */\r\n patRevoke: defineRoute('/pats/:patId/revoke'),\r\n\r\n /** Get PAT info from header (for authenticated requests) */\r\n patInfo: defineRoute('/pat-info'),\r\n\r\n // ============================================\r\n // Projects endpoints\r\n // ============================================\r\n\r\n /** List projects (GET) or create project (POST) */\r\n projects: defineRoute('/projects'),\r\n\r\n /** Get/update/delete specific project */\r\n project: defineRoute('/projects/:projectId'),\r\n\r\n /** Get jobs for a project */\r\n projectJobs: defineRoute('/projects/:projectId/jobs'),\r\n\r\n /** Get API keys for a project */\r\n projectApiKeys: defineRoute('/projects/:projectId/api-keys'),\r\n\r\n /** Transfer a project to another user or organization */\r\n projectTransfer: defineRoute('/projects/:projectId/transfer'),\r\n\r\n /** List templates for a project (GET) or create template (POST) */\r\n projectTemplates: defineRoute('/projects/:projectId/templates'),\r\n\r\n /** Get/update/delete specific template */\r\n projectTemplate: defineRoute('/projects/:projectId/templates/:templateId'),\r\n\r\n /** List flow charts for a project (GET) or upload flow chart (POST) */\r\n projectFlowCharts: defineRoute('/projects/:projectId/flowcharts'),\r\n\r\n /** Upload flow chart to a project */\r\n projectFlowChartsUpload: defineRoute('/projects/:projectId/flowcharts/upload'),\r\n\r\n /** Get/delete specific flow chart */\r\n projectFlowChart: defineRoute('/projects/:projectId/flowcharts/:flowchartId'),\r\n\r\n /** Get flow chart data */\r\n projectFlowChartData: defineRoute('/projects/:projectId/flowcharts/:flowchartId/data'),\r\n\r\n /** Chat with AI about a flow chart */\r\n projectFlowChartChat: defineRoute('/projects/:projectId/flowcharts/:flowchartId/chat'),\r\n\r\n /** Enable/disable public sharing for a flow chart */\r\n projectFlowChartShare: defineRoute('/projects/:projectId/flowcharts/:flowchartId/share'),\r\n\r\n /** List schedules for a project (GET) or create schedule (POST) */\r\n projectSchedules: defineRoute('/projects/:projectId/schedules'),\r\n\r\n /** Get/update/delete specific schedule */\r\n projectSchedule: defineRoute('/projects/:projectId/schedules/:scheduleId'),\r\n\r\n /** List execution history for a schedule */\r\n projectScheduleExecutions: defineRoute('/projects/:projectId/schedules/:scheduleId/executions'),\r\n\r\n /** Bulk create projects from GitHub repos */\r\n bulkCreateProjects: defineRoute('/projects/bulk'),\r\n\r\n // ============================================\r\n // GitHub endpoints\r\n // ============================================\r\n\r\n /** GitHub OAuth authorize */\r\n githubOAuthAuthorize: defineRoute('/github/oauth/authorize'),\r\n\r\n /** GitHub OAuth callback */\r\n githubCallback: defineRoute('/github/oauth/callback'),\r\n\r\n /** Link GitHub account (get GitHub username via OAuth) */\r\n githubLink: defineRoute('/github/link'),\r\n\r\n /** List issues for a repo (by owner/repo) */\r\n githubIssuesByRepo: defineRoute('/github/issues/:owner/:repo'),\r\n\r\n /** List issues (by projectId query param) */\r\n githubIssues: defineRoute('/github/issues'),\r\n\r\n /** Get a single issue */\r\n githubIssue: defineRoute('/github/issues/:issueNumber'),\r\n\r\n /** Get comments for an issue */\r\n githubIssueComments: defineRoute('/github/issues/:issueNumber/comments'),\r\n\r\n /** Get labels for a repo */\r\n githubIssueLabels: defineRoute('/github/issues/labels'),\r\n\r\n /** Get assignees for a repo */\r\n githubIssueAssignees: defineRoute('/github/issues/assignees'),\r\n\r\n /** Test a single issue */\r\n githubIssueTest: defineRoute('/github/issues/test'),\r\n\r\n /** Bulk test multiple issues */\r\n githubIssuesBulkTest: defineRoute('/github/issues/bulk-test'),\r\n\r\n /** List test sessions */\r\n githubTestSessions: defineRoute('/github/issues/test-sessions'),\r\n\r\n /** Get/delete a test session */\r\n githubTestSession: defineRoute('/github/issues/test-sessions/:sessionId'),\r\n\r\n /** Mark a test session as seen */\r\n githubTestSessionSeen: defineRoute('/github/issues/test-sessions/:sessionId/seen'),\r\n\r\n /** Get counts of running and unseen test sessions */\r\n githubTestSessionsCounts: defineRoute('/github/issues/test-sessions/counts'),\r\n\r\n /** Bulk test GitHub issues (legacy route) */\r\n githubBulkTest: defineRoute('/github/bulk-test'),\r\n\r\n /** GitHub App webhooks (receives issue_comment events etc.) */\r\n githubWebhooks: defineRoute('/github/webhooks'),\r\n\r\n // ============================================\r\n // Auth endpoints (Clerk-based)\r\n // ============================================\r\n\r\n /** Sync user data from Clerk after sign-in */\r\n authSync: defineRoute('/auth/sync'),\r\n\r\n /** Get current user info */\r\n authMe: defineRoute('/auth/me'),\r\n\r\n /** Mark user as startup (bonus tokens) */\r\n authStartup: defineRoute('/auth/startup'),\r\n\r\n /** Delete user account and all associated data */\r\n authDeleteAccount: defineRoute('/auth/account'),\r\n\r\n /** Preview account deletion impact (owned orgs, members, projects) */\r\n authDeletionPreview: defineRoute('/auth/account/deletion-preview'),\r\n\r\n // ============================================\r\n // Billing endpoints (Polar-based credits)\r\n // ============================================\r\n\r\n /** Get user's credit balance from Polar */\r\n billingBalance: defineRoute('/billing/balance'),\r\n\r\n /** Check if user has available credits */\r\n billingHasCredits: defineRoute('/billing/has-credits'),\r\n\r\n /** Get a customer portal URL for managing billing */\r\n billingPortal: defineRoute('/billing/portal'),\r\n\r\n /** Create a checkout session for purchasing credits */\r\n billingCheckout: defineRoute('/billing/checkout'),\r\n\r\n /** Get active subscription details (tier, cycle, etc.) */\r\n billingSubscription: defineRoute('/billing/subscription'),\r\n\r\n /** Update subscription plan (change tier/cycle) */\r\n billingChangePlan: defineRoute('/billing/change-plan'),\r\n\r\n // ============================================\r\n // Other endpoints\r\n // ============================================\r\n\r\n /** Health check */\r\n health: defineRoute('/health'),\r\n\r\n /** Detailed system status */\r\n status: defineRoute('/status'),\r\n\r\n /** List templates */\r\n templates: defineRoute('/templates'),\r\n\r\n /** Issue analyzer */\r\n issueAnalyzer: defineRoute('/issue-analyzer'),\r\n\r\n /** PR analyzer */\r\n prAnalyzer: defineRoute('/pr-analyzer'),\r\n\r\n /** Logs endpoint */\r\n logs: defineRoute('/logs'),\r\n\r\n // ============================================\r\n // Relevant Issues endpoints\r\n // ============================================\r\n\r\n /** Run relevant issues discovery for user */\r\n relevantIssuesDiscover: defineRoute('/relevant-issues/discover'),\r\n\r\n /** Get latest cached relevant issues result */\r\n relevantIssuesLatest: defineRoute('/relevant-issues/latest'),\r\n\r\n // ============================================\r\n // Onboarding endpoints\r\n // ============================================\r\n\r\n /** Get/update user's onboarding state */\r\n onboarding: defineRoute('/onboarding'),\r\n\r\n /** Mark onboarding as complete */\r\n onboardingComplete: defineRoute('/onboarding/complete'),\r\n\r\n /** Check if user needs onboarding */\r\n onboardingCheck: defineRoute('/onboarding/check'),\r\n\r\n // ============================================\r\n // Search endpoints\r\n // ============================================\r\n\r\n /** Global search across jobs, projects, issues, sessions */\r\n search: defineRoute('/search'),\r\n\r\n // ============================================\r\n // Tester Profile endpoints\r\n // ============================================\r\n\r\n /** Apply to become a tester */\r\n testerApply: defineRoute('/tester/apply'),\r\n\r\n /** Get/update own tester profile */\r\n testerProfile: defineRoute('/tester/profile'),\r\n\r\n /** Get public tester profile */\r\n testerPublicProfile: defineRoute('/testers/:testerId/profile'),\r\n\r\n /** Download tester app */\r\n testerDownloadApp: defineRoute('/tester/download-app'),\r\n\r\n /** Download browser extension */\r\n testerDownloadExtension: defineRoute('/tester/download-extension'),\r\n\r\n /** Download mobile tester app (Android APK) */\r\n testerDownloadMobileApp: defineRoute('/tester/download-mobile-app'),\r\n\r\n /** List available (unclaimed) jobs for testers */\r\n testerAvailableJobs: defineRoute('/tester/jobs/available'),\r\n\r\n /** Get presigned URL for avatar upload */\r\n testerAvatarUploadUrl: defineRoute('/tester/profile/avatar-upload-url'),\r\n\r\n /** Get tester app version */\r\n testerAppVersion: defineRoute('/tester/app-version'),\r\n\r\n /** Register/unregister Expo push token for mobile notifications */\r\n testerPushToken: defineRoute('/tester/push-token'),\r\n\r\n // ============================================\r\n // Extension endpoints (browser extension)\r\n // ============================================\r\n\r\n /** List extension tokens (GET) or create token (POST) */\r\n extensionTokens: defineRoute('/tester/extension-tokens'),\r\n\r\n /** Revoke (DELETE) a specific extension token */\r\n extensionToken: defineRoute('/tester/extension-tokens/:tokenId'),\r\n\r\n /** WebSocket endpoint for extension data streaming */\r\n extensionStream: defineRoute('/extension/stream'),\r\n\r\n /** Correlate extension data to a test job's time window */\r\n testerJobCorrelate: defineRoute('/tester/jobs/:jobId/correlate'),\r\n\r\n /** Check if extension data is flowing for this tester (polling endpoint) */\r\n testerJobDataStatus: defineRoute('/tester/jobs/:jobId/data-status'),\r\n\r\n /** Create extension token for auto-configuring the extension from the test page */\r\n testerJobExtensionToken: defineRoute('/tester/jobs/:jobId/extension-token'),\r\n\r\n // ============================================\r\n // Repo Templates endpoints\r\n // ============================================\r\n\r\n /** List templates from a GitHub repo's .runhuman/templates/ directory */\r\n repoTemplates: defineRoute('/repos/:owner/:repo/templates'),\r\n\r\n /** Get a single template from a GitHub repo */\r\n repoTemplate: defineRoute('/repos/:owner/:repo/templates/:templateName'),\r\n\r\n // ============================================\r\n // Organizations endpoints\r\n // ============================================\r\n\r\n /** List organizations (GET) or create organization (POST) */\r\n organizations: defineRoute('/organizations'),\r\n\r\n /** Get/update/delete specific organization */\r\n organization: defineRoute('/organizations/:organizationId'),\r\n\r\n /** List organization members */\r\n organizationMembers: defineRoute('/organizations/:organizationId/members'),\r\n\r\n /** Invite member to organization */\r\n organizationInvite: defineRoute('/organizations/:organizationId/invite'),\r\n\r\n /** Remove member from organization */\r\n organizationMember: defineRoute('/organizations/:organizationId/members/:userId'),\r\n\r\n /** List organization projects */\r\n organizationProjects: defineRoute('/organizations/:organizationId/projects'),\r\n\r\n /** Get organization jobs (jobs across all projects in the organization) */\r\n organizationJobs: defineRoute('/organizations/:organizationId/jobs'),\r\n\r\n /** Transfer organization ownership */\r\n organizationTransferOwnership: defineRoute('/organizations/:organizationId/transfer-ownership'),\r\n\r\n /** Create organization API key */\r\n organizationApiKeys: defineRoute('/organizations/:organizationId/api-keys'),\r\n\r\n /** Get GitHub installations for an organization */\r\n organizationGitHubInstallations: defineRoute('/organizations/:organizationId/github/installations'),\r\n\r\n /** Get/delete specific GitHub installation for an organization */\r\n organizationGitHubInstallation: defineRoute('/organizations/:organizationId/github/installations/:installationId'),\r\n\r\n /** List repos accessible to an organization via its GitHub installations */\r\n organizationGitHubRepos: defineRoute('/organizations/:organizationId/github/repos'),\r\n\r\n /** Refresh repos for an organization's GitHub installation */\r\n organizationGitHubInstallationRefresh: defineRoute('/organizations/:organizationId/github/installations/:installationId/refresh'),\r\n\r\n /** Check if org has access to a specific repo via its GitHub installations */\r\n organizationGitHubRepoCheckAccess: defineRoute('/organizations/:organizationId/github/repos/check-access'),\r\n\r\n /** Find deployed URL for a repo via org's GitHub installations */\r\n organizationGitHubRepoFindUrl: defineRoute('/organizations/:organizationId/github/repos/find-url'),\r\n\r\n /** Get all schedules across projects in the organization */\r\n organizationSchedules: defineRoute('/organizations/:organizationId/schedules'),\r\n\r\n /** Get organization billing balance (Polar) */\r\n organizationBilling: defineRoute('/organizations/:organizationId/billing'),\r\n\r\n // ============================================\r\n // Transfers endpoints\r\n // ============================================\r\n\r\n /** Get pending incoming transfers for current user */\r\n transfersPending: defineRoute('/transfers/pending'),\r\n\r\n /** Get pending outgoing transfers (initiated by current user) */\r\n transfersOutgoing: defineRoute('/transfers/outgoing'),\r\n\r\n /** Accept a transfer */\r\n transferAccept: defineRoute('/transfers/:transferId/accept'),\r\n\r\n /** Reject a transfer */\r\n transferReject: defineRoute('/transfers/:transferId/reject'),\r\n\r\n /** Cancel a transfer */\r\n transferCancel: defineRoute('/transfers/:transferId/cancel'),\r\n\r\n // ============================================\r\n // Invites endpoints (public)\r\n // ============================================\r\n\r\n /** Get invite details by token (public) */\r\n invite: defineRoute('/invites/:token'),\r\n\r\n /** Redeem an invite (authenticated) */\r\n inviteRedeem: defineRoute('/invites/:token/redeem'),\r\n\r\n // ============================================\r\n // User Agreements endpoints\r\n // ============================================\r\n\r\n /** Record user agreement acceptance (POST) or get user's agreements (GET) */\r\n agreements: defineRoute('/agreements'),\r\n\r\n /** Check if user has accepted a specific agreement */\r\n agreementCheck: defineRoute('/agreements/check'),\r\n\r\n // ============================================\r\n // Organization SSO endpoints\r\n // ============================================\r\n\r\n /** List SSO connections for an organization (GET) or create one (POST) */\r\n organizationSsoConnections: defineRoute('/organizations/:organizationId/sso/connections'),\r\n\r\n /** Get/delete a specific SSO connection */\r\n organizationSsoConnection: defineRoute('/organizations/:organizationId/sso/connections/:connectionId'),\r\n\r\n // ============================================\r\n // Enterprise endpoints\r\n // ============================================\r\n\r\n /** Submit enterprise contact inquiry (POST) */\r\n enterpriseInquiry: defineRoute('/enterprise/inquiry'),\r\n\r\n // ============================================\r\n // Changelog endpoints\r\n // ============================================\r\n\r\n /** List changelog entries (public) */\r\n changelog: defineRoute('/changelog'),\r\n\r\n /** Get unread changelog count (authenticated) */\r\n changelogUnreadCount: defineRoute('/changelog/unread-count'),\r\n\r\n /** Mark changelog as read (authenticated) */\r\n changelogMarkRead: defineRoute('/changelog/mark-read'),\r\n\r\n // ============================================\r\n // Telemetry SDK endpoints\r\n // ============================================\r\n\r\n /** Create telemetry session (POST) — SDK calls this when a test session starts */\r\n telemetrySessions: defineRoute('/telemetry/sessions'),\r\n\r\n /** End telemetry session (POST) — SDK calls this when testing completes */\r\n telemetrySessionEnd: defineRoute('/telemetry/sessions/:sessionId/end'),\r\n\r\n /** Submit telemetry event batch (POST) — SDK periodically flushes captured events */\r\n telemetryBatch: defineRoute('/telemetry/sessions/:sessionId/events'),\r\n\r\n /** Check session status for a job (GET) — SDK polls to know when to activate */\r\n telemetrySessionStatus: defineRoute('/telemetry/jobs/:jobId/status'),\r\n\r\n /** Resolve a sensor short code to a jobId (GET) — SDK calls this when tester enters a code */\r\n telemetryShortCodeResolve: defineRoute('/telemetry/short-codes/:code'),\r\n} as const;\r\n\r\n/**\r\n * Type for all API route names\r\n */\r\nexport type ApiRouteName = keyof typeof apiRoutes;\r\n","/**\r\n * Telemetry API Client\r\n *\r\n * Thin HTTP client wrapping the 4 Phase 0 telemetry endpoints.\r\n * Captures a reference to fetch at construction time — before the\r\n * network interceptor patches globalThis.fetch — to avoid recursion.\r\n */\r\n\r\nimport { apiRoutes, API_PREFIX } from '@runhuman/shared';\r\nimport type {\r\n CreateTelemetrySessionRequest,\r\n CreateTelemetrySessionResponse,\r\n TelemetryBatchRequest,\r\n TelemetryBatchResponse,\r\n EndTelemetrySessionResponse,\r\n TelemetrySessionStatusResponse,\r\n} from '@runhuman/shared';\r\n\r\nexport class ApiClient {\r\n private readonly baseUrl: string;\r\n private readonly apiKey: string;\r\n // Captured at construction time, before interceptors patch globalThis.fetch\r\n private readonly fetchFn: typeof globalThis.fetch;\r\n\r\n constructor(baseUrl: string, apiKey: string) {\r\n this.baseUrl = baseUrl.replace(/\\/$/, '');\r\n this.apiKey = apiKey;\r\n this.fetchFn = globalThis.fetch.bind(globalThis);\r\n }\r\n\r\n async createSession(\r\n req: CreateTelemetrySessionRequest,\r\n ): Promise<CreateTelemetrySessionResponse> {\r\n return this.post(\r\n apiRoutes.telemetrySessions.build(),\r\n req,\r\n );\r\n }\r\n\r\n async submitBatch(\r\n sessionId: string,\r\n req: TelemetryBatchRequest,\r\n ): Promise<TelemetryBatchResponse> {\r\n return this.post(\r\n apiRoutes.telemetryBatch.build({ sessionId }),\r\n req,\r\n );\r\n }\r\n\r\n async endSession(sessionId: string): Promise<EndTelemetrySessionResponse> {\r\n return this.post(\r\n apiRoutes.telemetrySessionEnd.build({ sessionId }),\r\n undefined,\r\n );\r\n }\r\n\r\n async getSessionStatus(jobId: string): Promise<TelemetrySessionStatusResponse> {\r\n return this.get(apiRoutes.telemetrySessionStatus.build({ jobId }));\r\n }\r\n\r\n async resolveShortCode(code: string): Promise<{ jobId: string } | null> {\r\n const normalized = code.replace(/^RH-/i, '').toUpperCase();\r\n try {\r\n return await this.get<{ jobId: string }>(\r\n apiRoutes.telemetryShortCodeResolve.build({ code: normalized }),\r\n );\r\n } catch (error) {\r\n if (error instanceof Error && error.message.includes('404')) return null;\r\n throw error;\r\n }\r\n }\r\n\r\n private url(routePath: string): string {\r\n return `${this.baseUrl}${API_PREFIX}${routePath}`;\r\n }\r\n\r\n private async post<T>(routePath: string, body: unknown): Promise<T> {\r\n const response = await this.fetchFn(this.url(routePath), {\r\n method: 'POST',\r\n headers: {\r\n 'Content-Type': 'application/json',\r\n Authorization: `Bearer ${this.apiKey}`,\r\n },\r\n body: body !== undefined ? JSON.stringify(body) : undefined,\r\n });\r\n\r\n if (!response.ok) {\r\n const text = await response.text();\r\n throw new Error(`Runhuman API error ${response.status}: ${text}`);\r\n }\r\n\r\n return response.json() as Promise<T>;\r\n }\r\n\r\n private async get<T>(routePath: string): Promise<T> {\r\n const response = await this.fetchFn(this.url(routePath), {\r\n method: 'GET',\r\n headers: {\r\n Authorization: `Bearer ${this.apiKey}`,\r\n },\r\n });\r\n\r\n if (!response.ok) {\r\n const text = await response.text();\r\n throw new Error(`Runhuman API error ${response.status}: ${text}`);\r\n }\r\n\r\n return response.json() as Promise<T>;\r\n }\r\n}\r\n","/**\r\n * Event Buffer\r\n *\r\n * In-memory ring buffer for telemetry events.\r\n * Events are pushed by interceptors and drained by the flush cycle.\r\n * FIFO eviction when at max capacity.\r\n */\r\n\r\nimport type { TelemetryBatchEvent } from '@runhuman/shared';\r\n\r\nexport class EventBuffer {\r\n private buffer: TelemetryBatchEvent[] = [];\r\n private readonly maxCapacity: number;\r\n\r\n constructor(maxCapacity = 1000) {\r\n this.maxCapacity = maxCapacity;\r\n }\r\n\r\n get size(): number {\r\n return this.buffer.length;\r\n }\r\n\r\n push(event: TelemetryBatchEvent): void {\r\n if (this.buffer.length >= this.maxCapacity) {\r\n this.buffer.shift();\r\n }\r\n this.buffer.push(event);\r\n }\r\n\r\n drain(): TelemetryBatchEvent[] {\r\n const events = this.buffer;\r\n this.buffer = [];\r\n return events;\r\n }\r\n\r\n clear(): void {\r\n this.buffer = [];\r\n }\r\n}\r\n","/**\r\n * Network Interceptor\r\n *\r\n * Patches globalThis.fetch to capture HTTP requests.\r\n * Privacy: strips Authorization, Cookie, Set-Cookie headers.\r\n * No request/response bodies are captured.\r\n */\r\n\r\nimport type { TelemetryBatchEvent } from '@runhuman/shared';\r\nimport type { EventCallback, Interceptor } from './types.js';\r\n\r\nconst STRIPPED_HEADERS = new Set([\r\n 'authorization',\r\n 'cookie',\r\n 'set-cookie',\r\n 'x-api-key',\r\n 'proxy-authorization',\r\n]);\r\n\r\nfunction scrubHeaders(headers: Headers): Record<string, string> {\r\n const result: Record<string, string> = {};\r\n headers.forEach((value, key) => {\r\n result[key] = STRIPPED_HEADERS.has(key.toLowerCase()) ? '[REDACTED]' : value;\r\n });\r\n return result;\r\n}\r\n\r\nexport class NetworkInterceptor implements Interceptor {\r\n private originalFetch: typeof globalThis.fetch | null = null;\r\n private sessionStartTime = 0;\r\n\r\n install(onEvent: EventCallback): void {\r\n this.originalFetch = globalThis.fetch;\r\n this.sessionStartTime = Date.now();\r\n\r\n const captured = this.originalFetch;\r\n const startTime = this.sessionStartTime;\r\n\r\n globalThis.fetch = async function interceptedFetch(\r\n input: RequestInfo | URL,\r\n init?: RequestInit,\r\n ): Promise<Response> {\r\n const requestStart = Date.now();\r\n const url = typeof input === 'string' ? input : input instanceof URL ? input.toString() : input.url;\r\n const method = init?.method ?? 'GET';\r\n\r\n const response = await captured.call(globalThis, input, init);\r\n\r\n const event: TelemetryBatchEvent = {\r\n type: 'network',\r\n relativeTimestampMs: requestStart - startTime,\r\n clientTimestamp: new Date(requestStart).toISOString(),\r\n data: {\r\n url,\r\n method: method.toUpperCase(),\r\n status: response.status,\r\n statusText: response.statusText,\r\n durationMs: Date.now() - requestStart,\r\n responseHeaders: scrubHeaders(response.headers),\r\n },\r\n };\r\n\r\n onEvent(event);\r\n\r\n return response;\r\n };\r\n }\r\n\r\n uninstall(): void {\r\n if (this.originalFetch) {\r\n globalThis.fetch = this.originalFetch;\r\n this.originalFetch = null;\r\n }\r\n }\r\n\r\n /** Update the session start time (called when a new session begins) */\r\n setSessionStartTime(startTime: number): void {\r\n this.sessionStartTime = startTime;\r\n }\r\n}\r\n","/**\r\n * Console Interceptor\r\n *\r\n * Patches console.log/warn/error/info/debug to capture log output.\r\n * Arguments are stringified to avoid capturing sensitive objects.\r\n */\r\n\r\nimport type { TelemetryBatchEvent } from '@runhuman/shared';\r\nimport type { EventCallback, Interceptor } from './types.js';\r\n\r\nconst CONSOLE_METHODS = ['log', 'warn', 'error', 'info', 'debug'] as const;\r\ntype ConsoleMethod = typeof CONSOLE_METHODS[number];\r\n\r\nfunction stringify(args: unknown[]): string {\r\n return args\r\n .map((arg) => {\r\n if (typeof arg === 'string') return arg;\r\n try {\r\n return JSON.stringify(arg);\r\n } catch {\r\n return String(arg);\r\n }\r\n })\r\n .join(' ');\r\n}\r\n\r\nexport class ConsoleInterceptor implements Interceptor {\r\n private originals: Partial<Record<ConsoleMethod, (...args: unknown[]) => void>> = {};\r\n private sessionStartTime = 0;\r\n\r\n install(onEvent: EventCallback): void {\r\n this.sessionStartTime = Date.now();\r\n const startTime = this.sessionStartTime;\r\n\r\n for (const method of CONSOLE_METHODS) {\r\n this.originals[method] = console[method].bind(console);\r\n\r\n console[method] = (...args: unknown[]) => {\r\n const now = Date.now();\r\n\r\n const event: TelemetryBatchEvent = {\r\n type: 'console',\r\n relativeTimestampMs: now - startTime,\r\n clientTimestamp: new Date(now).toISOString(),\r\n data: {\r\n level: method,\r\n message: stringify(args),\r\n },\r\n };\r\n\r\n onEvent(event);\r\n\r\n // Call the original so console output still appears\r\n this.originals[method]!(...args);\r\n };\r\n }\r\n }\r\n\r\n uninstall(): void {\r\n for (const method of CONSOLE_METHODS) {\r\n if (this.originals[method]) {\r\n console[method] = this.originals[method]!;\r\n }\r\n }\r\n this.originals = {};\r\n }\r\n\r\n setSessionStartTime(startTime: number): void {\r\n this.sessionStartTime = startTime;\r\n }\r\n}\r\n","/**\r\n * Error Interceptor\r\n *\r\n * Captures unhandled errors via:\r\n * - React Native's ErrorUtils.setGlobalHandler (when available)\r\n * - globalThis.onerror fallback (web/Node)\r\n *\r\n * Fatal errors emit 'crash', non-fatal emit 'error'.\r\n */\r\n\r\nimport type { TelemetryBatchEvent } from '@runhuman/shared';\r\nimport type { EventCallback, Interceptor } from './types.js';\r\n\r\n// React Native exposes ErrorUtils globally\r\ninterface ErrorUtilsType {\r\n getGlobalHandler(): (error: Error, isFatal: boolean) => void;\r\n setGlobalHandler(handler: (error: Error, isFatal: boolean) => void): void;\r\n}\r\n\r\ndeclare const ErrorUtils: ErrorUtilsType | undefined;\r\n\r\nexport class ErrorInterceptor implements Interceptor {\r\n private previousHandler: ((error: Error, isFatal: boolean) => void) | null = null;\r\n private previousOnError: typeof globalThis.onerror | null = null;\r\n private sessionStartTime = 0;\r\n\r\n install(onEvent: EventCallback): void {\r\n this.sessionStartTime = Date.now();\r\n const startTime = this.sessionStartTime;\r\n\r\n // React Native path\r\n if (typeof ErrorUtils !== 'undefined') {\r\n this.previousHandler = ErrorUtils.getGlobalHandler();\r\n const prev = this.previousHandler;\r\n\r\n ErrorUtils.setGlobalHandler((error: Error, isFatal: boolean) => {\r\n const now = Date.now();\r\n\r\n const event: TelemetryBatchEvent = {\r\n type: isFatal ? 'crash' : 'error',\r\n relativeTimestampMs: now - startTime,\r\n clientTimestamp: new Date(now).toISOString(),\r\n data: {\r\n message: error.message,\r\n stack: error.stack,\r\n name: error.name,\r\n isFatal,\r\n },\r\n };\r\n\r\n onEvent(event);\r\n\r\n // Forward to previous handler\r\n prev(error, isFatal);\r\n });\r\n return;\r\n }\r\n\r\n // Web/Node fallback\r\n this.previousOnError = globalThis.onerror;\r\n\r\n globalThis.onerror = (\r\n message: Event | string,\r\n source?: string,\r\n lineno?: number,\r\n colno?: number,\r\n error?: Error,\r\n ) => {\r\n const now = Date.now();\r\n\r\n const event: TelemetryBatchEvent = {\r\n type: 'error',\r\n relativeTimestampMs: now - startTime,\r\n clientTimestamp: new Date(now).toISOString(),\r\n data: {\r\n message: typeof message === 'string' ? message : message.type,\r\n stack: error?.stack,\r\n name: error?.name,\r\n source,\r\n lineno,\r\n colno,\r\n },\r\n };\r\n\r\n onEvent(event);\r\n\r\n if (typeof this.previousOnError === 'function') {\r\n return (this.previousOnError as (...args: unknown[]) => unknown).call(globalThis, message, source, lineno, colno, error);\r\n }\r\n };\r\n }\r\n\r\n uninstall(): void {\r\n if (typeof ErrorUtils !== 'undefined' && this.previousHandler) {\r\n ErrorUtils.setGlobalHandler(this.previousHandler);\r\n this.previousHandler = null;\r\n }\r\n\r\n if (this.previousOnError !== null) {\r\n globalThis.onerror = this.previousOnError;\r\n this.previousOnError = null;\r\n }\r\n }\r\n\r\n setSessionStartTime(startTime: number): void {\r\n this.sessionStartTime = startTime;\r\n }\r\n}\r\n","/**\r\n * Interceptor Manager\r\n *\r\n * Aggregates all interceptors into a single install/uninstall surface.\r\n */\r\n\r\nimport type { EventCallback, Interceptor } from './types.js';\r\nimport { NetworkInterceptor } from './network.interceptor.js';\r\nimport { ConsoleInterceptor } from './console.interceptor.js';\r\nimport { ErrorInterceptor } from './error.interceptor.js';\r\n\r\nexport class InterceptorManager implements Interceptor {\r\n private readonly interceptors: Interceptor[];\r\n\r\n readonly network: NetworkInterceptor;\r\n readonly console: ConsoleInterceptor;\r\n readonly error: ErrorInterceptor;\r\n\r\n constructor() {\r\n this.network = new NetworkInterceptor();\r\n this.console = new ConsoleInterceptor();\r\n this.error = new ErrorInterceptor();\r\n this.interceptors = [this.network, this.console, this.error];\r\n }\r\n\r\n install(onEvent: EventCallback): void {\r\n for (const interceptor of this.interceptors) {\r\n interceptor.install(onEvent);\r\n }\r\n }\r\n\r\n uninstall(): void {\r\n // Uninstall in reverse order\r\n for (let i = this.interceptors.length - 1; i >= 0; i--) {\r\n this.interceptors[i].uninstall();\r\n }\r\n }\r\n\r\n setSessionStartTime(startTime: number): void {\r\n this.network.setSessionStartTime(startTime);\r\n this.console.setSessionStartTime(startTime);\r\n this.error.setSessionStartTime(startTime);\r\n }\r\n}\r\n\r\nexport type { Interceptor, EventCallback } from './types.js';\r\n","/**\r\n * Session Manager\r\n *\r\n * Orchestrates the sensor lifecycle:\r\n * idle → polling → active → ending → idle\r\n *\r\n * - Polls the status endpoint to detect when a tester starts a job\r\n * - Creates a telemetry session and installs interceptors\r\n * - Periodically flushes buffered events to the API\r\n * - Ends the session when the job is no longer active\r\n */\r\n\r\nimport type { TelemetryPlatform } from '@runhuman/shared';\r\nimport type { ApiClient } from './api-client.js';\r\nimport { EventBuffer } from './event-buffer.js';\r\nimport { InterceptorManager } from './interceptors/index.js';\r\n\r\nexport type SessionState = 'idle' | 'polling' | 'active' | 'ending';\r\n\r\nexport interface SessionManagerConfig {\r\n apiClient: ApiClient;\r\n platform: TelemetryPlatform;\r\n sdkVersion: string;\r\n flushIntervalMs: number;\r\n pollIntervalMs: number;\r\n maxBufferSize: number;\r\n debug: boolean;\r\n}\r\n\r\nexport class SessionManager {\r\n private state: SessionState = 'idle';\r\n private sessionId: string | null = null;\r\n private activeJobId: string | null = null;\r\n private pollTimer: ReturnType<typeof setInterval> | null = null;\r\n private flushTimer: ReturnType<typeof setInterval> | null = null;\r\n private readonly buffer: EventBuffer;\r\n private readonly interceptors: InterceptorManager;\r\n private readonly config: SessionManagerConfig;\r\n private readonly listeners = new Set<() => void>();\r\n\r\n constructor(config: SessionManagerConfig) {\r\n this.config = config;\r\n this.buffer = new EventBuffer(config.maxBufferSize);\r\n this.interceptors = new InterceptorManager();\r\n }\r\n\r\n getState(): SessionState {\r\n return this.state;\r\n }\r\n\r\n /** Alias for getState() — named for React's useSyncExternalStore convention */\r\n getSnapshot(): SessionState {\r\n return this.state;\r\n }\r\n\r\n getSessionId(): string | null {\r\n return this.sessionId;\r\n }\r\n\r\n getActiveJobId(): string | null {\r\n return this.activeJobId;\r\n }\r\n\r\n /**\r\n * Subscribe to state changes. Returns an unsubscribe function.\r\n * Compatible with React's useSyncExternalStore.\r\n */\r\n subscribe(listener: () => void): () => void {\r\n this.listeners.add(listener);\r\n return () => this.listeners.delete(listener);\r\n }\r\n\r\n private setState(newState: SessionState): void {\r\n this.state = newState;\r\n for (const listener of this.listeners) {\r\n listener();\r\n }\r\n }\r\n\r\n /**\r\n * Start polling for job activation.\r\n * The sensor calls this on init — polling continues until a job is detected\r\n * or stopPolling() is called.\r\n */\r\n startPolling(jobId: string): void {\r\n if (this.state !== 'idle') return;\r\n\r\n this.activeJobId = jobId;\r\n this.setState('polling');\r\n this.log('Polling started', { jobId });\r\n\r\n this.pollTimer = setInterval(() => this.pollOnce(), this.config.pollIntervalMs);\r\n // Also poll immediately\r\n this.pollOnce();\r\n }\r\n\r\n stopPolling(): void {\r\n if (this.pollTimer) {\r\n clearInterval(this.pollTimer);\r\n this.pollTimer = null;\r\n }\r\n if (this.state === 'polling') {\r\n this.setState('idle');\r\n this.log('Polling stopped');\r\n }\r\n }\r\n\r\n /**\r\n * Activate a session immediately (e.g., from a deep link).\r\n * Skips polling — goes straight to session creation.\r\n */\r\n async activate(jobId: string): Promise<void> {\r\n if (this.state === 'active') {\r\n this.log('Already active, ignoring activate()', { jobId });\r\n return;\r\n }\r\n\r\n this.stopPolling();\r\n this.activeJobId = jobId;\r\n await this.startSession();\r\n }\r\n\r\n /**\r\n * End the current session and return to idle.\r\n */\r\n async deactivate(): Promise<void> {\r\n if (this.state !== 'active') return;\r\n await this.endSession();\r\n }\r\n\r\n /**\r\n * Full teardown — stop everything and clean up.\r\n */\r\n async destroy(): Promise<void> {\r\n this.stopPolling();\r\n if (this.state === 'active') {\r\n await this.endSession();\r\n }\r\n }\r\n\r\n private async pollOnce(): Promise<void> {\r\n if (this.state !== 'polling' || !this.activeJobId) return;\r\n\r\n try {\r\n const status = await this.config.apiClient.getSessionStatus(this.activeJobId);\r\n\r\n if (status.jobActive && !status.activeSessionId) {\r\n this.log('Job is active, creating session', {\r\n jobId: this.activeJobId,\r\n });\r\n this.stopPolling();\r\n await this.startSession();\r\n }\r\n } catch (error) {\r\n this.log('Poll failed', { error });\r\n }\r\n }\r\n\r\n private async startSession(): Promise<void> {\r\n this.setState('active');\r\n const now = new Date();\r\n\r\n const response = await this.config.apiClient.createSession({\r\n jobId: this.activeJobId!,\r\n platform: this.config.platform,\r\n sdkVersion: this.config.sdkVersion,\r\n clientStartTime: now.toISOString(),\r\n });\r\n\r\n this.sessionId = response.sessionId;\r\n const sessionStartTime = now.getTime();\r\n\r\n this.log('Session started', {\r\n sessionId: this.sessionId,\r\n clockOffsetMs: response.clockOffsetMs,\r\n });\r\n\r\n // Set the session start time on interceptors so relativeTimestampMs is correct\r\n this.interceptors.setSessionStartTime(sessionStartTime);\r\n\r\n // Install interceptors — events flow into the buffer\r\n this.interceptors.install((event) => this.buffer.push(event));\r\n\r\n // Start flush timer\r\n this.flushTimer = setInterval(() => this.flush(), this.config.flushIntervalMs);\r\n }\r\n\r\n private async endSession(): Promise<void> {\r\n this.setState('ending');\r\n\r\n // Stop flush timer and uninstall interceptors\r\n if (this.flushTimer) {\r\n clearInterval(this.flushTimer);\r\n this.flushTimer = null;\r\n }\r\n this.interceptors.uninstall();\r\n\r\n // Final flush\r\n await this.flush();\r\n\r\n // End the session on the server\r\n if (this.sessionId) {\r\n try {\r\n const result = await this.config.apiClient.endSession(this.sessionId);\r\n this.log('Session ended', {\r\n sessionId: this.sessionId,\r\n eventCount: result.eventCount,\r\n correlated: result.correlated,\r\n });\r\n } catch (error) {\r\n this.log('Failed to end session', { sessionId: this.sessionId, error });\r\n }\r\n }\r\n\r\n this.sessionId = null;\r\n this.activeJobId = null;\r\n this.buffer.clear();\r\n this.setState('idle');\r\n }\r\n\r\n private async flush(): Promise<void> {\r\n if (!this.sessionId) return;\r\n\r\n const events = this.buffer.drain();\r\n if (events.length === 0) return;\r\n\r\n try {\r\n const result = await this.config.apiClient.submitBatch(this.sessionId, { events });\r\n this.log('Flushed events', {\r\n accepted: result.accepted,\r\n sessionEventCount: result.sessionEventCount,\r\n });\r\n } catch (error) {\r\n this.log('Flush failed, events lost', { count: events.length, error });\r\n }\r\n }\r\n\r\n private log(message: string, data?: Record<string, unknown>): void {\r\n if (this.config.debug) {\r\n console.debug(`[Runhuman] ${message}`, data);\r\n }\r\n }\r\n}\r\n","/**\r\n * Deep Link Handler\r\n *\r\n * Listens for React Native deep links matching the pattern:\r\n * <scheme>://runhuman?jobId=<id>\r\n *\r\n * When matched, triggers immediate session activation via SessionManager.\r\n * Falls back gracefully when react-native Linking is unavailable.\r\n */\r\n\r\nimport type { SessionManager } from './session-manager.js';\r\n\r\n// React Native Linking API shape (avoid importing react-native at module level)\r\ninterface LinkingModule {\r\n addEventListener(type: string, handler: (event: { url: string }) => void): { remove(): void };\r\n getInitialURL(): Promise<string | null>;\r\n}\r\n\r\nexport class DeepLinkHandler {\r\n private subscription: { remove(): void } | null = null;\r\n private readonly sessionManager: SessionManager;\r\n private readonly debug: boolean;\r\n\r\n constructor(sessionManager: SessionManager, debug: boolean) {\r\n this.sessionManager = sessionManager;\r\n this.debug = debug;\r\n }\r\n\r\n async install(): Promise<void> {\r\n const Linking = this.getLinking();\r\n if (!Linking) {\r\n this.log('react-native Linking not available, deep links disabled');\r\n return;\r\n }\r\n\r\n // Handle deep links received while app is running\r\n this.subscription = Linking.addEventListener('url', ({ url }) => {\r\n this.handleUrl(url);\r\n });\r\n\r\n // Handle deep link that launched the app (cold start)\r\n const initialUrl = await Linking.getInitialURL();\r\n if (initialUrl) {\r\n this.handleUrl(initialUrl);\r\n }\r\n }\r\n\r\n uninstall(): void {\r\n if (this.subscription) {\r\n this.subscription.remove();\r\n this.subscription = null;\r\n }\r\n }\r\n\r\n private handleUrl(url: string): void {\r\n const jobId = this.extractJobId(url);\r\n if (!jobId) return;\r\n\r\n this.log('Deep link received', { url, jobId });\r\n this.sessionManager.activate(jobId);\r\n }\r\n\r\n /**\r\n * Extract jobId from a URL matching: <scheme>://runhuman?jobId=<id>\r\n * Returns null if the URL doesn't match the expected pattern.\r\n */\r\n private extractJobId(url: string): string | null {\r\n try {\r\n const parsed = new URL(url);\r\n // Match hostname OR pathname containing \"runhuman\"\r\n const isRunhuman =\r\n parsed.hostname === 'runhuman' ||\r\n parsed.pathname.includes('runhuman');\r\n\r\n if (!isRunhuman) return null;\r\n\r\n return parsed.searchParams.get('jobId');\r\n } catch {\r\n return null;\r\n }\r\n }\r\n\r\n private getLinking(): LinkingModule | null {\r\n try {\r\n // Dynamic require to avoid hard dependency on react-native\r\n const { Linking } = require('react-native');\r\n return Linking as LinkingModule;\r\n } catch {\r\n return null;\r\n }\r\n }\r\n\r\n private log(message: string, data?: Record<string, unknown>): void {\r\n if (this.debug) {\r\n console.debug(`[Runhuman] ${message}`, data);\r\n }\r\n }\r\n}\r\n","/**\r\n * Runhuman Sensor — Public API\r\n *\r\n * Singleton facade. Customers interact with this class only:\r\n *\r\n * Runhuman.init({ apiKey: 'rh_...' })\r\n *\r\n * When a Runhuman tester starts a job on an SDK-enabled app,\r\n * the sensor activates, captures events, and sends them to the backend.\r\n * Zero overhead when no test is running.\r\n */\r\n\r\nimport type { TelemetryPlatform } from '@runhuman/shared';\r\nimport { ApiClient } from './api-client.js';\r\nimport { SessionManager } from './session-manager.js';\r\nimport { DeepLinkHandler } from './deep-link-handler.js';\r\n\r\nconst PRODUCTION_BASE_URL = 'https://qa-experiment.fly.dev';\r\nconst SDK_VERSION = '0.1.0';\r\n\r\nexport interface RunhumanConfig {\r\n /** API key for authentication (e.g., 'rh_...') */\r\n apiKey: string;\r\n /** Base URL of the Runhuman API (defaults to production) */\r\n baseUrl?: string;\r\n /** Job ID if known upfront (e.g., from deep link params at app launch) */\r\n jobId?: string;\r\n /** Target platform (defaults to 'react-native') */\r\n platform?: TelemetryPlatform;\r\n /** How often to flush buffered events in ms (default: 5000) */\r\n flushIntervalMs?: number;\r\n /** How often to poll for job status in ms (default: 10000) */\r\n pollIntervalMs?: number;\r\n /** Max events in the ring buffer before FIFO eviction (default: 1000) */\r\n maxBufferSize?: number;\r\n /** Enable deep link activation (default: true) */\r\n enableDeepLinks?: boolean;\r\n /** Log debug info to console (default: false) */\r\n debug?: boolean;\r\n}\r\n\r\n/** Symbol key for the global singleton — survives duplicate bundles */\r\nconst INSTANCE_KEY = '__runhuman_sensor_instance__' as const;\r\n\r\nexport class Runhuman {\r\n private static get instance(): Runhuman | null {\r\n return (globalThis as Record<string, unknown>)[INSTANCE_KEY] as Runhuman | null ?? null;\r\n }\r\n\r\n private static set instance(value: Runhuman | null) {\r\n (globalThis as Record<string, unknown>)[INSTANCE_KEY] = value;\r\n }\r\n\r\n private readonly apiClient: ApiClient;\r\n private readonly sessionManager: SessionManager;\r\n private readonly deepLinkHandler: DeepLinkHandler | null;\r\n private readonly debug: boolean;\r\n\r\n private constructor(config: RunhumanConfig) {\r\n this.debug = config.debug === true;\r\n\r\n this.apiClient = new ApiClient(\r\n config.baseUrl ?? PRODUCTION_BASE_URL,\r\n config.apiKey,\r\n );\r\n\r\n this.sessionManager = new SessionManager({\r\n apiClient: this.apiClient,\r\n platform: config.platform ?? 'react-native',\r\n sdkVersion: SDK_VERSION,\r\n flushIntervalMs: config.flushIntervalMs ?? 5000,\r\n pollIntervalMs: config.pollIntervalMs ?? 10000,\r\n maxBufferSize: config.maxBufferSize ?? 1000,\r\n debug: this.debug,\r\n });\r\n\r\n // Deep link handler (optional)\r\n const enableDeepLinks = config.enableDeepLinks !== false;\r\n if (enableDeepLinks) {\r\n this.deepLinkHandler = new DeepLinkHandler(this.sessionManager, this.debug);\r\n this.deepLinkHandler.install();\r\n } else {\r\n this.deepLinkHandler = null;\r\n }\r\n\r\n this.log('Initialized');\r\n\r\n // If a jobId was provided, start polling immediately\r\n if (config.jobId) {\r\n this.sessionManager.startPolling(config.jobId);\r\n }\r\n }\r\n\r\n /**\r\n * Initialize the Runhuman sensor. Call once at app startup.\r\n *\r\n * @example\r\n * ```ts\r\n * Runhuman.init({ apiKey: 'rh_your_api_key' });\r\n * ```\r\n */\r\n static init(config: RunhumanConfig): Runhuman {\r\n if (Runhuman.instance) {\r\n throw new Error('Runhuman.init() called twice — use Runhuman.getInstance() or call destroy() first');\r\n }\r\n\r\n Runhuman.instance = new Runhuman(config);\r\n return Runhuman.instance;\r\n }\r\n\r\n /**\r\n * Get the existing Runhuman instance.\r\n * Throws if init() hasn't been called.\r\n */\r\n static getInstance(): Runhuman {\r\n if (!Runhuman.instance) {\r\n throw new Error('Runhuman not initialized — call Runhuman.init() first');\r\n }\r\n return Runhuman.instance;\r\n }\r\n\r\n /**\r\n * Manually activate a telemetry session for a specific job.\r\n * Use this when you receive a deep link or know the job ID.\r\n */\r\n activate(jobId: string): void {\r\n this.sessionManager.activate(jobId);\r\n }\r\n\r\n /**\r\n * Manually deactivate the current session.\r\n * Flushes remaining events and ends the session.\r\n */\r\n async deactivate(): Promise<void> {\r\n await this.sessionManager.deactivate();\r\n }\r\n\r\n /**\r\n * Full teardown — stops polling, ends any active session, cleans up listeners.\r\n * After calling destroy(), you can call init() again.\r\n */\r\n async destroy(): Promise<void> {\r\n await this.sessionManager.destroy();\r\n\r\n if (this.deepLinkHandler) {\r\n this.deepLinkHandler.uninstall();\r\n }\r\n\r\n Runhuman.instance = null;\r\n this.log('Destroyed');\r\n }\r\n\r\n /** Access the session manager (used by <RunhumanOverlay /> for reactive state) */\r\n getSessionManager(): SessionManager {\r\n return this.sessionManager;\r\n }\r\n\r\n /** Access the API client (used by <RunhumanOverlay /> for short code resolution) */\r\n getApiClient(): ApiClient {\r\n return this.apiClient;\r\n }\r\n\r\n private log(message: string): void {\r\n if (this.debug) {\r\n console.debug(`[Runhuman] ${message}`);\r\n }\r\n }\r\n}\r\n","/**\r\n * Short code entry panel for sensor activation.\r\n *\r\n * Floats above the app and lets testers type the RH-XXXX code\r\n * to activate telemetry capture. Dismissible to a small FAB.\r\n */\r\n\r\nimport React, { useState } from 'react';\r\nimport { View, Text, TextInput, Pressable } from 'react-native';\r\nimport { Runhuman } from '../runhuman.js';\r\nimport { overlayStyles as s } from './overlay-styles.js';\r\n\r\nexport interface CodeEntryPanelProps {\r\n position: 'topLeft' | 'topRight' | 'bottomLeft' | 'bottomRight';\r\n}\r\n\r\nexport function CodeEntryPanel({ position }: CodeEntryPanelProps) {\r\n const [code, setCode] = useState('');\r\n const [error, setError] = useState<string | null>(null);\r\n const [resolving, setResolving] = useState(false);\r\n const [minimized, setMinimized] = useState(false);\r\n\r\n const handleSubmit = async () => {\r\n const trimmed = code.trim();\r\n if (!trimmed) return;\r\n\r\n setResolving(true);\r\n setError(null);\r\n\r\n const instance = Runhuman.getInstance();\r\n const result = await instance.getApiClient().resolveShortCode(trimmed);\r\n\r\n if (result) {\r\n instance.activate(result.jobId);\r\n } else {\r\n setError('Invalid or expired code');\r\n setResolving(false);\r\n }\r\n };\r\n\r\n if (minimized) {\r\n return (\r\n <Pressable style={[s.fab, s[position]]} onPress={() => setMinimized(false)}>\r\n <Text style={s.fabText}>RH</Text>\r\n </Pressable>\r\n );\r\n }\r\n\r\n return (\r\n <View style={[s.panel, s[position]]}>\r\n <Pressable style={s.minimizeButton} onPress={() => setMinimized(true)}>\r\n <Text style={s.minimizeText}>-</Text>\r\n </Pressable>\r\n\r\n <Text style={s.panelTitle}>Runhuman Sensor</Text>\r\n\r\n <TextInput\r\n style={error ? { ...s.input, ...s.inputError } : s.input}\r\n value={code}\r\n onChangeText={(text) => {\r\n setCode(text.toUpperCase());\r\n setError(null);\r\n }}\r\n placeholder=\"RH-XXXX\"\r\n placeholderTextColor=\"rgba(255,255,255,0.3)\"\r\n autoCapitalize=\"characters\"\r\n maxLength={10}\r\n editable={!resolving}\r\n onSubmitEditing={handleSubmit}\r\n />\r\n\r\n {error ? <Text style={s.errorText}>{error}</Text> : null}\r\n\r\n <Pressable\r\n style={resolving ? { ...s.submitButton, ...s.submitButtonDisabled } : s.submitButton}\r\n onPress={handleSubmit}\r\n disabled={resolving || !code.trim()}\r\n >\r\n <Text style={s.submitButtonText}>\r\n {resolving ? 'Activating...' : 'Activate'}\r\n </Text>\r\n </Pressable>\r\n </View>\r\n );\r\n}\r\n","/**\r\n * Shared styles for the RunhumanOverlay components.\r\n * Uses React Native StyleSheet for cross-platform consistency.\r\n */\r\n\r\nimport { StyleSheet } from 'react-native';\r\n\r\nconst BRAND_GREEN = '#22c55e';\r\nconst BRAND_AMBER = '#f59e0b';\r\nconst BRAND_RED = '#ef4444';\r\nconst BRAND_BLUE = '#3b82f6';\r\nconst OVERLAY_BG = 'rgba(0, 0, 0, 0.85)';\r\nconst OVERLAY_BORDER = 'rgba(255, 255, 255, 0.15)';\r\n\r\nexport const overlayStyles = StyleSheet.create({\r\n // Positioning containers\r\n topLeft: { top: 60, left: 16 },\r\n topRight: { top: 60, right: 16 },\r\n bottomLeft: { bottom: 40, left: 16 },\r\n bottomRight: { bottom: 40, right: 16 },\r\n\r\n // Code entry panel\r\n panel: {\r\n position: 'absolute',\r\n zIndex: 99999,\r\n backgroundColor: OVERLAY_BG,\r\n borderRadius: 12,\r\n borderWidth: 1,\r\n borderColor: OVERLAY_BORDER,\r\n padding: 16,\r\n width: 220,\r\n },\r\n panelTitle: {\r\n color: '#ffffff',\r\n fontSize: 13,\r\n fontWeight: '600',\r\n marginBottom: 8,\r\n },\r\n input: {\r\n backgroundColor: 'rgba(255, 255, 255, 0.1)',\r\n borderRadius: 8,\r\n borderWidth: 1,\r\n borderColor: OVERLAY_BORDER,\r\n color: '#ffffff',\r\n fontSize: 18,\r\n fontFamily: 'monospace',\r\n fontWeight: 'bold',\r\n letterSpacing: 2,\r\n padding: 10,\r\n textAlign: 'center',\r\n },\r\n inputError: {\r\n borderColor: BRAND_RED,\r\n },\r\n submitButton: {\r\n backgroundColor: BRAND_BLUE,\r\n borderRadius: 8,\r\n paddingVertical: 8,\r\n marginTop: 8,\r\n alignItems: 'center' as const,\r\n },\r\n submitButtonDisabled: {\r\n opacity: 0.5,\r\n },\r\n submitButtonText: {\r\n color: '#ffffff',\r\n fontSize: 13,\r\n fontWeight: '600',\r\n },\r\n errorText: {\r\n color: BRAND_RED,\r\n fontSize: 11,\r\n marginTop: 4,\r\n textAlign: 'center' as const,\r\n },\r\n minimizeButton: {\r\n position: 'absolute' as const,\r\n top: 8,\r\n right: 8,\r\n },\r\n minimizeText: {\r\n color: 'rgba(255, 255, 255, 0.5)',\r\n fontSize: 16,\r\n },\r\n\r\n // Minimized fab (floating action button)\r\n fab: {\r\n position: 'absolute' as const,\r\n zIndex: 99999,\r\n width: 40,\r\n height: 40,\r\n borderRadius: 20,\r\n backgroundColor: OVERLAY_BG,\r\n borderWidth: 1,\r\n borderColor: OVERLAY_BORDER,\r\n alignItems: 'center' as const,\r\n justifyContent: 'center' as const,\r\n },\r\n fabText: {\r\n color: '#ffffff',\r\n fontSize: 16,\r\n },\r\n\r\n // Active indicator dot\r\n indicator: {\r\n position: 'absolute' as const,\r\n zIndex: 99999,\r\n width: 12,\r\n height: 12,\r\n borderRadius: 6,\r\n },\r\n indicatorActive: {\r\n backgroundColor: BRAND_GREEN,\r\n },\r\n indicatorEnding: {\r\n backgroundColor: BRAND_AMBER,\r\n },\r\n indicatorPolling: {\r\n backgroundColor: BRAND_BLUE,\r\n },\r\n});\r\n","/**\r\n * Small floating dot that shows the sensor's recording state.\r\n *\r\n * - Green pulsing dot when active (recording telemetry)\r\n * - Amber dot when ending session\r\n * - Blue dot when polling for a job\r\n */\r\n\r\nimport React, { useEffect, useRef } from 'react';\r\nimport { Animated } from 'react-native';\r\nimport type { SessionState } from '../session-manager.js';\r\nimport { overlayStyles as s } from './overlay-styles.js';\r\n\r\nexport interface ActiveIndicatorProps {\r\n state: SessionState;\r\n position: 'topLeft' | 'topRight' | 'bottomLeft' | 'bottomRight';\r\n}\r\n\r\nconst stateStyle = {\r\n active: s.indicatorActive,\r\n ending: s.indicatorEnding,\r\n polling: s.indicatorPolling,\r\n} as const;\r\n\r\nexport function ActiveIndicator({ state, position }: ActiveIndicatorProps) {\r\n const pulse = useRef(new Animated.Value(1)).current;\r\n\r\n useEffect(() => {\r\n if (state === 'active') {\r\n const animation = Animated.loop(\r\n Animated.sequence([\r\n Animated.timing(pulse, { toValue: 0.3, duration: 800, useNativeDriver: true }),\r\n Animated.timing(pulse, { toValue: 1, duration: 800, useNativeDriver: true }),\r\n ]),\r\n );\r\n animation.start();\r\n return () => animation.stop();\r\n }\r\n pulse.setValue(1);\r\n }, [state, pulse]);\r\n\r\n const colorStyle = stateStyle[state as keyof typeof stateStyle];\r\n if (!colorStyle) return null;\r\n\r\n return (\r\n <Animated.View style={[s.indicator, s[position], colorStyle, { opacity: pulse }]} />\r\n );\r\n}\r\n"],"mappings":"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","names":["View","StyleSheet","jsx","jsx","jsxs","View","StyleSheet"]}
|
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
import { CreateTelemetrySessionRequest, CreateTelemetrySessionResponse, TelemetryBatchRequest, TelemetryBatchResponse, EndTelemetrySessionResponse, TelemetrySessionStatusResponse, TelemetryPlatform } from '@runhuman/shared';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Telemetry API Client
|
|
5
|
-
*
|
|
6
|
-
* Thin HTTP client wrapping the 4 Phase 0 telemetry endpoints.
|
|
7
|
-
* Captures a reference to fetch at construction time — before the
|
|
8
|
-
* network interceptor patches globalThis.fetch — to avoid recursion.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
declare class ApiClient {
|
|
12
|
-
private readonly baseUrl;
|
|
13
|
-
private readonly apiKey;
|
|
14
|
-
private readonly fetchFn;
|
|
15
|
-
constructor(baseUrl: string, apiKey: string);
|
|
16
|
-
createSession(req: CreateTelemetrySessionRequest): Promise<CreateTelemetrySessionResponse>;
|
|
17
|
-
submitBatch(sessionId: string, req: TelemetryBatchRequest): Promise<TelemetryBatchResponse>;
|
|
18
|
-
endSession(sessionId: string): Promise<EndTelemetrySessionResponse>;
|
|
19
|
-
getSessionStatus(jobId: string): Promise<TelemetrySessionStatusResponse>;
|
|
20
|
-
resolveShortCode(code: string): Promise<{
|
|
21
|
-
jobId: string;
|
|
22
|
-
} | null>;
|
|
23
|
-
private url;
|
|
24
|
-
private post;
|
|
25
|
-
private get;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Session Manager
|
|
30
|
-
*
|
|
31
|
-
* Orchestrates the sensor lifecycle:
|
|
32
|
-
* idle → polling → active → ending → idle
|
|
33
|
-
*
|
|
34
|
-
* - Polls the status endpoint to detect when a tester starts a job
|
|
35
|
-
* - Creates a telemetry session and installs interceptors
|
|
36
|
-
* - Periodically flushes buffered events to the API
|
|
37
|
-
* - Ends the session when the job is no longer active
|
|
38
|
-
*/
|
|
39
|
-
|
|
40
|
-
type SessionState = 'idle' | 'polling' | 'active' | 'ending';
|
|
41
|
-
interface SessionManagerConfig {
|
|
42
|
-
apiClient: ApiClient;
|
|
43
|
-
platform: TelemetryPlatform;
|
|
44
|
-
sdkVersion: string;
|
|
45
|
-
flushIntervalMs: number;
|
|
46
|
-
pollIntervalMs: number;
|
|
47
|
-
maxBufferSize: number;
|
|
48
|
-
debug: boolean;
|
|
49
|
-
}
|
|
50
|
-
declare class SessionManager {
|
|
51
|
-
private state;
|
|
52
|
-
private sessionId;
|
|
53
|
-
private activeJobId;
|
|
54
|
-
private pollTimer;
|
|
55
|
-
private flushTimer;
|
|
56
|
-
private readonly buffer;
|
|
57
|
-
private readonly interceptors;
|
|
58
|
-
private readonly config;
|
|
59
|
-
private readonly listeners;
|
|
60
|
-
constructor(config: SessionManagerConfig);
|
|
61
|
-
getState(): SessionState;
|
|
62
|
-
/** Alias for getState() — named for React's useSyncExternalStore convention */
|
|
63
|
-
getSnapshot(): SessionState;
|
|
64
|
-
getSessionId(): string | null;
|
|
65
|
-
getActiveJobId(): string | null;
|
|
66
|
-
/**
|
|
67
|
-
* Subscribe to state changes. Returns an unsubscribe function.
|
|
68
|
-
* Compatible with React's useSyncExternalStore.
|
|
69
|
-
*/
|
|
70
|
-
subscribe(listener: () => void): () => void;
|
|
71
|
-
private setState;
|
|
72
|
-
/**
|
|
73
|
-
* Start polling for job activation.
|
|
74
|
-
* The sensor calls this on init — polling continues until a job is detected
|
|
75
|
-
* or stopPolling() is called.
|
|
76
|
-
*/
|
|
77
|
-
startPolling(jobId: string): void;
|
|
78
|
-
stopPolling(): void;
|
|
79
|
-
/**
|
|
80
|
-
* Activate a session immediately (e.g., from a deep link).
|
|
81
|
-
* Skips polling — goes straight to session creation.
|
|
82
|
-
*/
|
|
83
|
-
activate(jobId: string): Promise<void>;
|
|
84
|
-
/**
|
|
85
|
-
* End the current session and return to idle.
|
|
86
|
-
*/
|
|
87
|
-
deactivate(): Promise<void>;
|
|
88
|
-
/**
|
|
89
|
-
* Full teardown — stop everything and clean up.
|
|
90
|
-
*/
|
|
91
|
-
destroy(): Promise<void>;
|
|
92
|
-
private pollOnce;
|
|
93
|
-
private startSession;
|
|
94
|
-
private endSession;
|
|
95
|
-
private flush;
|
|
96
|
-
private log;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
export { ApiClient as A, SessionManager as S, type SessionState as a };
|
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
import { CreateTelemetrySessionRequest, CreateTelemetrySessionResponse, TelemetryBatchRequest, TelemetryBatchResponse, EndTelemetrySessionResponse, TelemetrySessionStatusResponse, TelemetryPlatform } from '@runhuman/shared';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Telemetry API Client
|
|
5
|
-
*
|
|
6
|
-
* Thin HTTP client wrapping the 4 Phase 0 telemetry endpoints.
|
|
7
|
-
* Captures a reference to fetch at construction time — before the
|
|
8
|
-
* network interceptor patches globalThis.fetch — to avoid recursion.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
declare class ApiClient {
|
|
12
|
-
private readonly baseUrl;
|
|
13
|
-
private readonly apiKey;
|
|
14
|
-
private readonly fetchFn;
|
|
15
|
-
constructor(baseUrl: string, apiKey: string);
|
|
16
|
-
createSession(req: CreateTelemetrySessionRequest): Promise<CreateTelemetrySessionResponse>;
|
|
17
|
-
submitBatch(sessionId: string, req: TelemetryBatchRequest): Promise<TelemetryBatchResponse>;
|
|
18
|
-
endSession(sessionId: string): Promise<EndTelemetrySessionResponse>;
|
|
19
|
-
getSessionStatus(jobId: string): Promise<TelemetrySessionStatusResponse>;
|
|
20
|
-
resolveShortCode(code: string): Promise<{
|
|
21
|
-
jobId: string;
|
|
22
|
-
} | null>;
|
|
23
|
-
private url;
|
|
24
|
-
private post;
|
|
25
|
-
private get;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Session Manager
|
|
30
|
-
*
|
|
31
|
-
* Orchestrates the sensor lifecycle:
|
|
32
|
-
* idle → polling → active → ending → idle
|
|
33
|
-
*
|
|
34
|
-
* - Polls the status endpoint to detect when a tester starts a job
|
|
35
|
-
* - Creates a telemetry session and installs interceptors
|
|
36
|
-
* - Periodically flushes buffered events to the API
|
|
37
|
-
* - Ends the session when the job is no longer active
|
|
38
|
-
*/
|
|
39
|
-
|
|
40
|
-
type SessionState = 'idle' | 'polling' | 'active' | 'ending';
|
|
41
|
-
interface SessionManagerConfig {
|
|
42
|
-
apiClient: ApiClient;
|
|
43
|
-
platform: TelemetryPlatform;
|
|
44
|
-
sdkVersion: string;
|
|
45
|
-
flushIntervalMs: number;
|
|
46
|
-
pollIntervalMs: number;
|
|
47
|
-
maxBufferSize: number;
|
|
48
|
-
debug: boolean;
|
|
49
|
-
}
|
|
50
|
-
declare class SessionManager {
|
|
51
|
-
private state;
|
|
52
|
-
private sessionId;
|
|
53
|
-
private activeJobId;
|
|
54
|
-
private pollTimer;
|
|
55
|
-
private flushTimer;
|
|
56
|
-
private readonly buffer;
|
|
57
|
-
private readonly interceptors;
|
|
58
|
-
private readonly config;
|
|
59
|
-
private readonly listeners;
|
|
60
|
-
constructor(config: SessionManagerConfig);
|
|
61
|
-
getState(): SessionState;
|
|
62
|
-
/** Alias for getState() — named for React's useSyncExternalStore convention */
|
|
63
|
-
getSnapshot(): SessionState;
|
|
64
|
-
getSessionId(): string | null;
|
|
65
|
-
getActiveJobId(): string | null;
|
|
66
|
-
/**
|
|
67
|
-
* Subscribe to state changes. Returns an unsubscribe function.
|
|
68
|
-
* Compatible with React's useSyncExternalStore.
|
|
69
|
-
*/
|
|
70
|
-
subscribe(listener: () => void): () => void;
|
|
71
|
-
private setState;
|
|
72
|
-
/**
|
|
73
|
-
* Start polling for job activation.
|
|
74
|
-
* The sensor calls this on init — polling continues until a job is detected
|
|
75
|
-
* or stopPolling() is called.
|
|
76
|
-
*/
|
|
77
|
-
startPolling(jobId: string): void;
|
|
78
|
-
stopPolling(): void;
|
|
79
|
-
/**
|
|
80
|
-
* Activate a session immediately (e.g., from a deep link).
|
|
81
|
-
* Skips polling — goes straight to session creation.
|
|
82
|
-
*/
|
|
83
|
-
activate(jobId: string): Promise<void>;
|
|
84
|
-
/**
|
|
85
|
-
* End the current session and return to idle.
|
|
86
|
-
*/
|
|
87
|
-
deactivate(): Promise<void>;
|
|
88
|
-
/**
|
|
89
|
-
* Full teardown — stop everything and clean up.
|
|
90
|
-
*/
|
|
91
|
-
destroy(): Promise<void>;
|
|
92
|
-
private pollOnce;
|
|
93
|
-
private startSession;
|
|
94
|
-
private endSession;
|
|
95
|
-
private flush;
|
|
96
|
-
private log;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
export { ApiClient as A, SessionManager as S, type SessionState as a };
|