Skip to content

Commit cf3a9c4

Browse files
authored
Add external data source signal sources (GitHub, Linear, Zendesk) (#1262)
<!-- CURSOR_AGENT_PR_BODY_BEGIN --> ## Problem Signal sources only supported session replay and LLM analytics. We want users to also pull in GitHub issues, Linear issues, and Zendesk tickets as signal sources - but these require connecting an external data source first (OAuth for GitHub/Linear, API key for Zendesk). ## Changes ## ![CleanShot 2026-03-16 at 20.12.52@2x.png](https://app.graphite.com/user-attachments/assets/118c5d6a-9a24-4482-9bd7-6c1d3f818801.png) Changes, as described by Claude: - Use `/api/projects/...` consistently for external data source schema updates (`updateExternalDataSchema`) - Add new OAuth scopes for external data sources and bump scope version - Build `DataSourceSetup` component with per-source setup flows (GitHub repo picker, Linear OAuth + polling, Zendesk credentials form) - Extend `SignalSourceToggles` to show a "Connect" button for sources that need setup before they can be toggled - Extract signal source management logic into `useSignalSourceManager` hook (replaces the old `signalSourceSelectionsStore` and the inline logic that was duplicated between settings and onboarding) + update `SignalsStep`, `SignalSourcesSettings`, and `useTutorialTour` to use the new hook
1 parent 647fce7 commit cf3a9c4

12 files changed

Lines changed: 966 additions & 276 deletions

File tree

apps/code/src/renderer/api/posthogClient.ts

Lines changed: 100 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,34 @@ export type McpServerInstallation = Schemas.MCPServerInstallation;
2020

2121
export interface SignalSourceConfig {
2222
id: string;
23-
source_product: "session_replay" | "llm_analytics";
24-
source_type: "session_analysis_cluster" | "evaluation";
23+
source_product:
24+
| "session_replay"
25+
| "llm_analytics"
26+
| "github"
27+
| "linear"
28+
| "zendesk";
29+
source_type: "session_analysis_cluster" | "evaluation" | "issue" | "ticket";
2530
enabled: boolean;
2631
config: Record<string, unknown>;
2732
created_at: string;
2833
updated_at: string;
2934
}
3035

36+
export interface ExternalDataSourceSchema {
37+
id: string;
38+
name: string;
39+
should_sync: boolean;
40+
}
41+
42+
export interface ExternalDataSource {
43+
id: string;
44+
source_type: string;
45+
status: string;
46+
// The generated `ExternalDataSourceSerializers` types this as `string`,
47+
// but the actual API returns an array of schema objects
48+
schemas?: ExternalDataSourceSchema[] | string;
49+
}
50+
3151
function isObjectRecord(value: unknown): value is Record<string, unknown> {
3252
return typeof value === "object" && value !== null;
3353
}
@@ -199,8 +219,17 @@ export class PostHogAPIClient {
199219
async createSignalSourceConfig(
200220
projectId: number,
201221
options: {
202-
source_product: "session_replay" | "llm_analytics";
203-
source_type: "session_analysis_cluster" | "evaluation";
222+
source_product:
223+
| "session_replay"
224+
| "llm_analytics"
225+
| "github"
226+
| "linear"
227+
| "zendesk";
228+
source_type:
229+
| "session_analysis_cluster"
230+
| "evaluation"
231+
| "issue"
232+
| "ticket";
204233
enabled: boolean;
205234
config?: Record<string, unknown>;
206235
},
@@ -254,6 +283,73 @@ export class PostHogAPIClient {
254283
return (await response.json()) as SignalSourceConfig;
255284
}
256285

286+
async listExternalDataSources(
287+
projectId: number,
288+
): Promise<ExternalDataSource[]> {
289+
const data = (await this.api.get(
290+
"/api/projects/{project_id}/external_data_sources/",
291+
{
292+
path: { project_id: projectId.toString() },
293+
query: {},
294+
},
295+
)) as unknown as { results?: ExternalDataSource[] } | ExternalDataSource[];
296+
return Array.isArray(data) ? data : (data.results ?? []);
297+
}
298+
299+
async createExternalDataSource(
300+
projectId: number,
301+
payload: {
302+
source_type: string;
303+
payload: Record<string, unknown>;
304+
},
305+
): Promise<ExternalDataSource> {
306+
const response = await this.api.post(
307+
"/api/projects/{project_id}/external_data_sources/",
308+
{
309+
path: { project_id: projectId.toString() },
310+
body: payload as unknown as Schemas.ExternalDataSourceSerializers,
311+
withResponse: true,
312+
throwOnStatusError: false,
313+
},
314+
);
315+
if (!response.ok) {
316+
const errorData = isObjectRecord(response.data)
317+
? (response.data as { detail?: string })
318+
: {};
319+
throw new Error(
320+
errorData.detail ??
321+
`Failed to create external data source: ${response.statusText}`,
322+
);
323+
}
324+
return response.data as unknown as ExternalDataSource;
325+
}
326+
327+
async updateExternalDataSchema(
328+
projectId: number,
329+
schemaId: string,
330+
updates: { should_sync: boolean },
331+
): Promise<void> {
332+
const urlPath = `/api/projects/${projectId}/external_data_schemas/${schemaId}/`;
333+
const url = new URL(`${this.api.baseUrl}${urlPath}`);
334+
const response = await this.api.fetcher.fetch({
335+
method: "patch",
336+
url,
337+
path: urlPath,
338+
overrides: {
339+
body: JSON.stringify(updates),
340+
},
341+
});
342+
if (!response.ok) {
343+
const errorData = (await response.json().catch(() => ({}))) as {
344+
detail?: string;
345+
};
346+
throw new Error(
347+
errorData.detail ??
348+
`Failed to update external data schema: ${response.statusText}`,
349+
);
350+
}
351+
}
352+
257353
async getTasks(options?: {
258354
repository?: string;
259355
createdBy?: number;

0 commit comments

Comments
 (0)