Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
180 changes: 180 additions & 0 deletions apps/code/src/renderer/api/posthogClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -600,6 +600,10 @@ export class PostHogAPIClient {
throw new Error("No team found for user");
}

async getDefaultProjectId(): Promise<number> {
return this.getTeamId();
}
Comment on lines +603 to +605
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 getDefaultProjectId does nothing but delegate to getTeamId. The one caller in posthogChip.ts could call getTeamId() directly (with its own String() cast), or getDefaultProjectId could at least drop the unnecessary async keyword since it is a single return of an already-Promise-returning method.

Suggested change
async getDefaultProjectId(): Promise<number> {
return this.getTeamId();
}
/** Returns the numeric team/project ID to use when a PostHog URL omits the project segment. */
getDefaultProjectId(): Promise<number> {
return this.getTeamId();
}
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/code/src/renderer/api/posthogClient.ts
Line: 603-605

Comment:
`getDefaultProjectId` does nothing but delegate to `getTeamId`. The one caller in `posthogChip.ts` could call `getTeamId()` directly (with its own `String()` cast), or `getDefaultProjectId` could at least drop the unnecessary `async` keyword since it is a single `return` of an already-`Promise`-returning method.

```suggestion
  /** Returns the numeric team/project ID to use when a PostHog URL omits the project segment. */
  getDefaultProjectId(): Promise<number> {
    return this.getTeamId();
  }
```

How can I resolve this? If you propose a fix, please make it concise.


async getCurrentUser() {
const data = await this.api.get("/api/users/{uuid}/", {
path: { uuid: "@me" },
Expand Down Expand Up @@ -2887,4 +2891,180 @@ export class PostHogAPIClient {
}
return (await response.json()) as SpendAnalysisResponse;
}

async getFeatureFlag(
projectId: string,
flagId: string,
): Promise<{ name: string; key: string } | null> {
const urlPath = `/api/projects/${encodeURIComponent(projectId)}/feature_flags/${encodeURIComponent(flagId)}/`;
const url = new URL(`${this.api.baseUrl}${urlPath}`);
const response = await this.api.fetcher.fetch({
method: "get",
url,
path: urlPath,
});
if (!response.ok) return null;
const data = (await response.json()) as { name?: string; key?: string };
return { name: data.name ?? "", key: data.key ?? "" };
}

async getExperiment(
projectId: string,
experimentId: string,
): Promise<{ name: string } | null> {
const urlPath = `/api/projects/${encodeURIComponent(projectId)}/experiments/${encodeURIComponent(experimentId)}/`;
const url = new URL(`${this.api.baseUrl}${urlPath}`);
const response = await this.api.fetcher.fetch({
method: "get",
url,
path: urlPath,
});
if (!response.ok) return null;
const data = (await response.json()) as { name?: string };
return { name: data.name ?? "" };
}

async getInsight(
projectId: string,
insightId: string,
): Promise<{ name: string } | null> {
const urlPath = `/api/projects/${encodeURIComponent(projectId)}/insights/${encodeURIComponent(insightId)}/`;
const url = new URL(`${this.api.baseUrl}${urlPath}`);
const response = await this.api.fetcher.fetch({
method: "get",
url,
path: urlPath,
});
if (!response.ok) return null;
const data = (await response.json()) as { name?: string };
return { name: data.name ?? "" };
}

async getDashboard(
projectId: string,
dashboardId: string,
): Promise<{ name: string } | null> {
const urlPath = `/api/projects/${encodeURIComponent(projectId)}/dashboards/${encodeURIComponent(dashboardId)}/`;
const url = new URL(`${this.api.baseUrl}${urlPath}`);
const response = await this.api.fetcher.fetch({
method: "get",
url,
path: urlPath,
});
if (!response.ok) return null;
const data = (await response.json()) as { name?: string };
return { name: data.name ?? "" };
}

async getErrorTrackingGroup(
projectId: string,
groupId: string,
): Promise<{ title: string } | null> {
const urlPath = `/api/projects/${encodeURIComponent(projectId)}/error_tracking/${encodeURIComponent(groupId)}/`;
const url = new URL(`${this.api.baseUrl}${urlPath}`);
const response = await this.api.fetcher.fetch({
method: "get",
url,
path: urlPath,
});
if (!response.ok) return null;
const data = (await response.json()) as { title?: string };
return { title: data.title ?? "" };
}

async getRecording(
projectId: string,
recordingId: string,
): Promise<{ name: string } | null> {
const urlPath = `/api/projects/${encodeURIComponent(projectId)}/session_recordings/${encodeURIComponent(recordingId)}/`;
const url = new URL(`${this.api.baseUrl}${urlPath}`);
const response = await this.api.fetcher.fetch({
method: "get",
url,
path: urlPath,
});
if (!response.ok) return null;
const data = (await response.json()) as { name?: string };
return { name: data.name ?? "" };
}

async getSurvey(
projectId: string,
surveyId: string,
): Promise<{ name: string } | null> {
const urlPath = `/api/projects/${encodeURIComponent(projectId)}/surveys/${encodeURIComponent(surveyId)}/`;
const url = new URL(`${this.api.baseUrl}${urlPath}`);
const response = await this.api.fetcher.fetch({
method: "get",
url,
path: urlPath,
});
if (!response.ok) return null;
const data = (await response.json()) as { name?: string };
return { name: data.name ?? "" };
}

async getNotebook(
projectId: string,
notebookId: string,
): Promise<{ title: string } | null> {
const urlPath = `/api/projects/${encodeURIComponent(projectId)}/notebooks/${encodeURIComponent(notebookId)}/`;
const url = new URL(`${this.api.baseUrl}${urlPath}`);
const response = await this.api.fetcher.fetch({
method: "get",
url,
path: urlPath,
});
if (!response.ok) return null;
const data = (await response.json()) as { title?: string };
return { title: data.title ?? "" };
}

async getCohort(
projectId: string,
cohortId: string,
): Promise<{ name: string } | null> {
const urlPath = `/api/projects/${encodeURIComponent(projectId)}/cohorts/${encodeURIComponent(cohortId)}/`;
const url = new URL(`${this.api.baseUrl}${urlPath}`);
const response = await this.api.fetcher.fetch({
method: "get",
url,
path: urlPath,
});
if (!response.ok) return null;
const data = (await response.json()) as { name?: string };
return { name: data.name ?? "" };
}

async getAction(
projectId: string,
actionId: string,
): Promise<{ name: string } | null> {
const urlPath = `/api/projects/${encodeURIComponent(projectId)}/actions/${encodeURIComponent(actionId)}/`;
const url = new URL(`${this.api.baseUrl}${urlPath}`);
const response = await this.api.fetcher.fetch({
method: "get",
url,
path: urlPath,
});
if (!response.ok) return null;
const data = (await response.json()) as { name?: string };
return { name: data.name ?? "" };
}

async getEarlyAccessFeature(
projectId: string,
featureId: string,
): Promise<{ name: string } | null> {
const urlPath = `/api/projects/${encodeURIComponent(projectId)}/early_access_feature/${encodeURIComponent(featureId)}/`;
const url = new URL(`${this.api.baseUrl}${urlPath}`);
const response = await this.api.fetcher.fetch({
method: "get",
url,
path: urlPath,
});
if (!response.ok) return null;
const data = (await response.json()) as { name?: string };
return { name: data.name ?? "" };
}
}
Comment on lines 2891 to 3070
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Repeated fetch pattern violates OnceAndOnlyOnce

All 11 new methods share an identical 6-line body: build urlPath, construct a URL, call this.api.fetcher.fetch, return null on non-ok, cast the JSON, and return the relevant field. A private helper (e.g. fetchProjectResource(endpoint, projectId, resourceId): Promise<unknown>) would let each public method reduce to a one-liner field-pluck, making future endpoint additions or error-handling changes apply in one place.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/code/src/renderer/api/posthogClient.ts
Line: 2891-3070

Comment:
**Repeated fetch pattern violates OnceAndOnlyOnce**

All 11 new methods share an identical 6-line body: build `urlPath`, construct a `URL`, call `this.api.fetcher.fetch`, return `null` on non-ok, cast the JSON, and return the relevant field. A private helper (e.g. `fetchProjectResource(endpoint, projectId, resourceId): Promise<unknown>`) would let each public method reduce to a one-liner field-pluck, making future endpoint additions or error-handling changes apply in one place.

How can I resolve this? If you propose a fix, please make it concise.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,29 @@ import { CodeBlock } from "@components/CodeBlock";
import { Divider } from "@components/Divider";
import { HighlightedCode } from "@components/HighlightedCode";
import { List, ListItem } from "@components/List";
import { parseGithubIssueUrl } from "@features/message-editor/utils/githubIssueUrl";
import {
type ParsedGithubIssueUrl,
parseGithubIssueUrl,
} from "@features/message-editor/utils/githubIssueUrl";
import {
buildResolvedLabel,
fetchPostHogResourceTitle,
} from "@features/message-editor/utils/posthogChip";
import {
type ParsedPostHogUrl,
parsePostHogUrl,
} from "@features/message-editor/utils/posthogUrl";
import { useAuthenticatedQuery } from "@hooks/useAuthenticatedQuery";
import { Blockquote, Checkbox, Code, Kbd, Text } from "@radix-ui/themes";
import { useTRPC } from "@renderer/trpc/client";
import { useQuery } from "@tanstack/react-query";
import { memo, useMemo } from "react";
import type { Components } from "react-markdown";
import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm";
import type { PluggableList } from "unified";
import { GithubRefChip } from "./GithubRefChip";
import { PostHogRefChip } from "./PostHogRefChip";

interface MarkdownRendererProps {
content: string;
Expand All @@ -30,6 +45,54 @@ const HeadingText = ({ children }: { children: React.ReactNode }) => (
</Text>
);

function SmartGithubRefChip({ parsed }: { parsed: ParsedGithubIssueUrl }) {
const trpc = useTRPC();
const input = {
owner: parsed.owner,
repo: parsed.repo,
number: parsed.number,
};
const options =
parsed.kind === "pr"
? trpc.git.getGithubPullRequest.queryOptions(input)
: trpc.git.getGithubIssue.queryOptions(input);
const { data } = useQuery({ ...options, staleTime: 60_000 });

const label = data?.title
? `#${parsed.number} - ${data.title}`
: `${parsed.owner}/${parsed.repo}#${parsed.number}`;

return (
<GithubRefChip href={parsed.normalizedUrl} kind={parsed.kind}>
{label}
</GithubRefChip>
);
}

function SmartPostHogRefChip({ parsed }: { parsed: ParsedPostHogUrl }) {
const { data: title } = useAuthenticatedQuery(
[
"posthog-resource",
parsed.resourceType,
parsed.projectId,
parsed.resourceId,
],
(client) => fetchPostHogResourceTitle(client, parsed),
{ staleTime: 60_000 },
);

const label = buildResolvedLabel(parsed, title ?? null);

return (
<PostHogRefChip
href={parsed.normalizedUrl}
resourceType={parsed.resourceType}
>
{label}
</PostHogRefChip>
);
}

export const baseComponents: Components = {
h1: ({ children }) => <HeadingText>{children}</HeadingText>,
h2: ({ children }) => <HeadingText>{children}</HeadingText>,
Expand Down Expand Up @@ -83,15 +146,30 @@ export const baseComponents: Components = {
const githubRef = href ? parseGithubIssueUrl(href) : null;
if (githubRef) {
const isAutoLink = typeof children === "string" && children === href;
const label = isAutoLink
? `${githubRef.owner}/${githubRef.repo}#${githubRef.number}`
: children;
if (isAutoLink) {
return <SmartGithubRefChip parsed={githubRef} />;
}
return (
<GithubRefChip href={githubRef.normalizedUrl} kind={githubRef.kind}>
{label}
{children}
</GithubRefChip>
);
}
const posthogRef = href ? parsePostHogUrl(href) : null;
if (posthogRef) {
const isAutoLink = typeof children === "string" && children === href;
if (isAutoLink) {
return <SmartPostHogRefChip parsed={posthogRef} />;
}
return (
<PostHogRefChip
href={posthogRef.normalizedUrl}
resourceType={posthogRef.resourceType}
>
{children}
</PostHogRefChip>
);
}
return (
<a
href={href}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import type { PostHogResourceType } from "@features/message-editor/utils/posthogUrl";
import {
BugIcon,
ChartLineIcon,
ClipboardTextIcon,
FlagIcon,
FlaskIcon,
LightningIcon,
NotebookIcon,
RocketLaunchIcon,
SquaresFourIcon,
UsersThreeIcon,
VideoIcon,
} from "@phosphor-icons/react";
import { Chip } from "@posthog/quill";
import type { ComponentType, ReactNode } from "react";

const resourceIconMap: Record<
PostHogResourceType,
ComponentType<{ size: number }>
> = {
feature_flag: FlagIcon,
experiment: FlaskIcon,
insight: ChartLineIcon,
dashboard: SquaresFourIcon,
error_tracking: BugIcon,
recording: VideoIcon,
survey: ClipboardTextIcon,
notebook: NotebookIcon,
cohort: UsersThreeIcon,
action: LightningIcon,
early_access_feature: RocketLaunchIcon,
};

export function PostHogRefChip({
href,
resourceType,
children,
}: {
href: string;
resourceType: PostHogResourceType;
children: ReactNode;
}) {
const Icon = resourceIconMap[resourceType];
return (
<Chip
size="xs"
onClick={() => window.open(href, "_blank")}
className="cli-file-mention mx-0.5 max-w-full cursor-pointer! whitespace-nowrap pl-1 align-middle active:translate-y-0"
>
<Icon size={10} />
<span className="min-w-0 truncate">{children}</span>
</Chip>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,14 @@ export type ChipType =
| "experiment"
| "insight"
| "feature_flag"
| "dashboard"
| "recording"
| "error_tracking"
| "survey"
| "notebook"
| "cohort"
| "action"
| "early_access_feature"
| "github_issue"
| "github_pr";

Expand Down
Loading