Compare commits

...
7 Commits
Author SHA1 Message Date
Marc Klingen 8584f93307 chore: release v2.30.2
CI/CD / lint (push) Waiting to run
CI/CD / test-docker-build (push) Waiting to run
CI/CD / tests-web (20) (push) Waiting to run
CI/CD / tests-worker (20) (push) Waiting to run
CI/CD / e2e-tests (push) Waiting to run
CI/CD / all-ci-passed (push) Blocked by required conditions
CI/CD / push-docker-image (push) Blocked by required conditions
release.yml / release (push) Waiting to run
2024-04-19 18:05:17 +02:00
Marc KlingenandGitHub 08e576f815 feat(cloud): add settings for posthog integration (#1778) 2024-04-19 17:38:55 +02:00
Marc KlingenandGitHub f64da44b20 chore: update docs links in settings (#1786) 2024-04-19 10:49:26 +00:00
Marc KlingenandGitHub c720a4d896 perf: reduce memory usage of users.all trpc route (#1784) 2024-04-19 10:03:06 +00:00
Marc Klingen b59b61a5cf chore: release v2.30.1
CI/CD / lint (push) Waiting to run
CI/CD / test-docker-build (push) Waiting to run
CI/CD / tests-web (20) (push) Waiting to run
CI/CD / tests-worker (20) (push) Waiting to run
CI/CD / e2e-tests (push) Waiting to run
CI/CD / all-ci-passed (push) Blocked by required conditions
CI/CD / push-docker-image (push) Blocked by required conditions
release.yml / release (push) Waiting to run
2024-04-19 01:07:09 +02:00
Marc KlingenandGitHub 3f18e42fef feat(api): add countObservations to metrics endpoint, model nullable (#1779) 2024-04-18 23:04:29 +00:00
Marc KlingenandGitHub 31a9be6f1d fix(api): support special characters in dataset name and run name (#1762) 2024-04-18 22:49:10 +00:00
29 changed files with 509 additions and 90 deletions
+2 -8
View File
@@ -1,15 +1,9 @@
import { z } from "zod";
import { env as sharedEnv } from "@langfuse/shared";
const EnvSchema = z.object({
NEXT_PUBLIC_LANGFUSE_CLOUD_REGION: z.string().optional(),
ENCRYPTION_KEY: z
.string()
.length(
64,
"ENCRYPTION_KEY must be 256 bits, 64 string characters in hex format, generate via: openssl rand -hex 32"
)
.optional(),
ADMIN_API_KEY: z.string().optional(),
});
export const env = EnvSchema.parse(process.env);
export const env = { ...sharedEnv, ...EnvSchema.parse(process.env) };
+1 -1
View File
@@ -6,8 +6,8 @@ import Auth0Provider from "next-auth/providers/auth0";
import AzureADProvider from "next-auth/providers/azure-ad";
import { isEeAvailable } from "..";
import { prisma } from "@langfuse/shared/src/db";
import { encrypt, decrypt } from "@langfuse/shared/encryption";
import { SsoProviderSchema } from "./types";
import { decrypt, encrypt } from "../encryption";
import { type NextApiRequest, type NextApiResponse } from "next";
import { env } from "../env";
+2 -1
View File
@@ -47,12 +47,13 @@ types:
properties:
date: date
countTraces: integer
countObservations: integer
totalCost: double
usage: list<UsageByModel>
UsageByModel:
docs: Daily usage of a given model. Usage corresponds to the unit set for the specific model (e.g. tokens).
properties:
model: string
model: optional<string>
inputUsage: integer
outputUsage: integer
totalUsage: integer
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "langfuse",
"version": "2.30.0",
"version": "2.30.2",
"author": "engineering@langfuse.com",
"license": "MIT",
"private": true,
+5
View File
@@ -20,6 +20,10 @@
"./src/server/auth": {
"import": "./dist/src/server/auth.js",
"require": "./dist/src/server/auth.js"
},
"./encryption": {
"import": "./dist/src/encryption/index.js",
"require": "./dist/src/encryption/index.js"
}
},
"scripts": {
@@ -28,6 +32,7 @@
"lint": "eslint . --ext .js,.jsx,.ts,.tsx",
"lint:fix": "eslint . --ext .js,.jsx,.ts,.tsx --fix",
"db:migrate": "DISABLE_ERD=false dotenv -e ../../.env -- npx prisma migrate dev",
"db:push": "DISABLE_ERD=false dotenv -e ../../.env -- npx prisma db push",
"db:reset": "dotenv -e ../../.env npx -- prisma migrate reset",
"db:deploy": "dotenv -e ../../.env npx -- prisma migrate deploy",
"db:seed": "dotenv -e ../../.env -- npx prisma db seed",
@@ -292,6 +292,13 @@ export type ObservationView = {
calculated_total_cost: string | null;
latency: number | null;
};
export type PosthogIntegration = {
project_id: string;
encrypted_posthog_api_key: string;
posthog_host_name: string;
last_sync_at: Timestamp | null;
enabled: boolean;
};
export type Pricing = {
id: string;
model_name: string;
@@ -422,6 +429,7 @@ export type DB = {
models: Model;
observations: Observation;
observations_view: ObservationView;
posthog_integrations: PosthogIntegration;
pricings: Pricing;
projects: Project;
prompts: Prompt;
@@ -0,0 +1,16 @@
-- CreateTable
CREATE TABLE "posthog_integrations" (
"project_id" TEXT NOT NULL,
"encrypted_posthog_api_key" TEXT NOT NULL,
"posthog_host_name" TEXT NOT NULL,
"last_sync_at" TIMESTAMP(3),
"enabled" BOOLEAN NOT NULL,
CONSTRAINT "posthog_integrations_pkey" PRIMARY KEY ("project_id")
);
-- CreateIndex
CREATE INDEX "posthog_integrations_project_id_idx" ON "posthog_integrations"("project_id");
-- AddForeignKey
ALTER TABLE "posthog_integrations" ADD CONSTRAINT "posthog_integrations_project_id_fkey" FOREIGN KEY ("project_id") REFERENCES "projects"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+32 -19
View File
@@ -92,25 +92,26 @@ model VerificationToken {
}
model Project {
id String @id @default(cuid())
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
name String
cloudConfig Json? @map("cloud_config") // Langfuse Cloud, for zod schema see projectsRouter.ts
members Membership[]
traces Trace[]
observations Observation[]
apiKeys ApiKey[]
dataset Dataset[]
RawEvents Events[]
invitations MembershipInvitation[]
sessions TraceSession[]
Prompt Prompt[]
Model Model[]
AuditLog AuditLog[]
EvalTemplate EvalTemplate[]
JobConfiguration JobConfiguration[]
JobExecution JobExecution[]
id String @id @default(cuid())
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
name String
cloudConfig Json? @map("cloud_config") // Langfuse Cloud, for zod schema see projectsRouter.ts
members Membership[]
traces Trace[]
observations Observation[]
apiKeys ApiKey[]
dataset Dataset[]
RawEvents Events[]
invitations MembershipInvitation[]
sessions TraceSession[]
Prompt Prompt[]
Model Model[]
AuditLog AuditLog[]
EvalTemplate EvalTemplate[]
JobConfiguration JobConfiguration[]
JobExecution JobExecution[]
PosthogIntegration PosthogIntegration[]
@@map("projects")
}
@@ -699,3 +700,15 @@ model SsoConfig {
@@map("sso_configs")
}
model PosthogIntegration {
projectId String @id @map("project_id")
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
encryptedPosthogApiKey String @map("encrypted_posthog_api_key")
posthogHostName String @map("posthog_host_name")
lastSyncAt DateTime? @map("last_sync_at")
enabled Boolean
@@index([projectId])
@@map("posthog_integrations")
}
+13
View File
@@ -0,0 +1,13 @@
import { z } from "zod";
const EnvSchema = z.object({
ENCRYPTION_KEY: z
.string()
.length(
64,
"ENCRYPTION_KEY must be 256 bits, 64 string characters in hex format, generate via: openssl rand -hex 32"
)
.optional(),
});
export const env = EnvSchema.parse(process.env);
+1
View File
@@ -9,6 +9,7 @@ export * from "./filterToPrisma";
export * from "./tracesTable";
export * from "./server/auth";
export * from "./observationsTable";
export { env } from "./env";
// llm api
export * from "./server/llm/types";
+4 -1
View File
@@ -2397,6 +2397,8 @@ components:
type: string
countTraces:
type: integer
countObservations:
type: integer
totalCost:
type: number
format: double
@@ -2407,6 +2409,7 @@ components:
required:
- date
- countTraces
- countObservations
- totalCost
- usage
UsageByModel:
@@ -2418,6 +2421,7 @@ components:
properties:
model:
type: string
nullable: true
inputUsage:
type: integer
outputUsage:
@@ -2425,7 +2429,6 @@ components:
totalUsage:
type: integer
required:
- model
- inputUsage
- outputUsage
- totalUsage
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "web",
"version": "2.30.0",
"version": "2.30.2",
"private": true,
"license": "MIT",
"engines": {
+16 -15
View File
@@ -49,16 +49,16 @@ describe("/api/public/datasets and /api/public/dataset-items API Endpoints", ()
});
afterEach(async () => await pruneDatabase());
it("should create and get a dataset", async () => {
it("should create and get a dataset, include special characters", async () => {
await makeAPICall("POST", "/api/public/datasets", {
name: "dataset-name",
name: "dataset + name",
description: "dataset-description",
metadata: { foo: "bar" },
});
const dbDataset = await prisma.dataset.findMany({
where: {
name: "dataset-name",
name: "dataset + name",
},
});
@@ -66,12 +66,12 @@ describe("/api/public/datasets and /api/public/dataset-items API Endpoints", ()
const getDataset = await makeAPICall(
"GET",
`/api/public/datasets/dataset-name`,
`/api/public/datasets/${encodeURIComponent("dataset + name")}`,
);
expect(getDataset.status).toBe(200);
expect(getDataset.body).toMatchObject({
name: "dataset-name",
name: "dataset + name",
description: "dataset-description",
metadata: { foo: "bar" },
});
@@ -225,20 +225,20 @@ describe("/api/public/datasets and /api/public/dataset-items API Endpoints", ()
expect(dbDatasetItem?.metadata).toMatchObject(["hello-world"]);
});
it("should create and get a dataset run", async () => {
it("should create and get a dataset run, include special characters", async () => {
const dataset = await makeAPICall<{ id: string }>(
"POST",
"/api/public/datasets",
{
name: "dataset-name",
name: "dataset name",
},
);
expect(dataset.status).toBe(200);
expect(dataset.body).toMatchObject({
name: "dataset-name",
name: "dataset name",
});
await makeAPICall("POST", "/api/public/dataset-items", {
datasetName: "dataset-name",
datasetName: "dataset name",
id: "dataset-item-id",
input: { key: "value" },
expectedOutput: { key: "value" },
@@ -287,14 +287,14 @@ describe("/api/public/datasets and /api/public/dataset-items API Endpoints", ()
{
datasetItemId: "dataset-item-id",
observationId: observationId,
runName: "run-only-observation",
runName: "run + only + observation",
runDescription: "run-description",
metadata: { key: "value" },
},
);
const dbRunObservation = await prisma.datasetRuns.findFirst({
where: {
name: "run-only-observation",
name: "run + only + observation",
},
include: {
datasetRunItems: true,
@@ -313,21 +313,22 @@ describe("/api/public/datasets and /api/public/dataset-items API Endpoints", ()
const getRunAPI = await makeAPICall(
"GET",
`/api/public/datasets/dataset-name/runs/run-only-observation`,
`/api/public/datasets/${encodeURIComponent("dataset name")}/runs/${encodeURIComponent("run + only + observation")}`,
);
expect(getRunAPI.status).toBe(200);
expect(getRunAPI.body).toMatchObject({
name: "run-only-observation",
name: "run + only + observation",
description: "run-description",
metadata: { key: "value" },
datasetId: dataset.body.id,
datasetName: "dataset-name",
datasetName: "dataset name",
datasetRunItems: expect.arrayContaining([
expect.objectContaining({
datasetItemId: "dataset-item-id",
observationId: observationId,
traceId: traceId,
datasetRunName: "run-only-observation",
datasetRunName: "run + only + observation",
}),
]),
});
+2 -1
View File
@@ -18,6 +18,7 @@ export default function Header({
featureBetaURL?: string;
actionButtons?: React.ReactNode;
level?: "h2" | "h3";
className?: string;
}) {
const router = useRouter();
const session = useSession();
@@ -42,7 +43,7 @@ export default function Header({
[...props.breadcrumb.map((i) => i.href).filter(Boolean)].pop();
return (
<div className={cn(level === "h2" ? "mb-4" : "mb-2")}>
<div className={cn(level === "h2" ? "mb-4" : "mb-2", props.className)}>
<div>
{backHref ? (
<nav className="sm:hidden" aria-label="Back">
+1 -1
View File
@@ -1 +1 @@
export const VERSION = "v2.30.0";
export const VERSION = "v2.30.2";
+2 -1
View File
@@ -15,7 +15,8 @@ export type AuditableResource =
| "session"
| "apiKey"
| "evalTemplate"
| "job";
| "job"
| "posthogIntegration";
type AuditLog = {
resourceType: AuditableResource;
@@ -0,0 +1,117 @@
import { z } from "zod";
import { auditLog } from "@/src/features/audit-logs/auditLog";
import { throwIfNoAccess } from "@/src/features/rbac/utils/checkAccess";
import {
createTRPCRouter,
protectedProjectProcedure,
} from "@/src/server/api/trpc";
import { decrypt, encrypt } from "@langfuse/shared/encryption";
import { posthogIntegrationFormSchema } from "@/src/features/posthog-integration/types";
import { TRPCError } from "@trpc/server";
export const posthogIntegrationRouter = createTRPCRouter({
get: protectedProjectProcedure
.input(z.object({ projectId: z.string() }))
.query(async ({ input, ctx }) => {
throwIfNoAccess({
session: ctx.session,
projectId: input.projectId,
scope: "integrations:CRUD",
});
try {
const dbConfig = await ctx.prisma.posthogIntegration.findFirst({
where: {
projectId: input.projectId,
},
});
if (!dbConfig) {
return null;
}
const { encryptedPosthogApiKey, ...config } = dbConfig;
return {
...config,
posthogApiKey: decrypt(encryptedPosthogApiKey),
};
} catch (e) {
console.error("posthog integration get", e);
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
});
}
}),
update: protectedProjectProcedure
.input(posthogIntegrationFormSchema.extend({ projectId: z.string() }))
.mutation(async ({ input, ctx }) => {
try {
throwIfNoAccess({
session: ctx.session,
projectId: input.projectId,
scope: "integrations:CRUD",
});
await auditLog({
session: ctx.session,
action: "update",
resourceType: "posthogIntegration",
resourceId: input.projectId,
});
const { posthogProjectApiKey, ...config } = input;
const encryptedPosthogApiKey = encrypt(posthogProjectApiKey);
await ctx.prisma.posthogIntegration.upsert({
where: {
projectId: input.projectId,
},
create: {
projectId: input.projectId,
posthogHostName: config.posthogHostname,
encryptedPosthogApiKey,
enabled: config.enabled,
},
update: {
encryptedPosthogApiKey,
posthogHostName: config.posthogHostname,
enabled: config.enabled,
},
});
} catch (e) {
console.log("posthog integration update", e);
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
});
}
}),
delete: protectedProjectProcedure
.input(z.object({ projectId: z.string() }))
.mutation(async ({ input, ctx }) => {
try {
throwIfNoAccess({
session: ctx.session,
projectId: input.projectId,
scope: "integrations:CRUD",
});
await auditLog({
session: ctx.session,
action: "delete",
resourceType: "posthogIntegration",
resourceId: input.projectId,
});
await ctx.prisma.posthogIntegration.delete({
where: {
projectId: input.projectId,
},
});
} catch (e) {
console.log("posthog integration delete", e);
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
});
}
}),
});
@@ -0,0 +1,9 @@
import { z } from "zod";
export const posthogIntegrationFormSchema = z.object({
posthogHostname: z.string().url(),
posthogProjectApiKey: z.string().refine((v) => v.startsWith("phc_"), {
message: "PostHog Project API Key must start with 'phc_'",
}),
enabled: z.boolean(),
});
@@ -20,6 +20,7 @@ const scopes = [
"project:delete",
"project:update",
"project:transfer",
"integrations:CRUD",
"datasets:CUD",
@@ -45,6 +46,7 @@ export const roleAccessRights: Record<MembershipRole, Scope[]> = {
"apiKeys:read",
"apiKeys:create",
"apiKeys:delete",
"integrations:CRUD",
"objects:publish",
"objects:bookmark",
"objects:tag",
@@ -70,6 +72,7 @@ export const roleAccessRights: Record<MembershipRole, Scope[]> = {
"apiKeys:read",
"apiKeys:create",
"apiKeys:delete",
"integrations:CRUD",
"objects:publish",
"objects:bookmark",
"objects:tag",
@@ -6,7 +6,7 @@ import { verifyAuthHeaderAndReturnScope } from "@/src/features/public-api/server
import { isPrismaException } from "@/src/utils/exceptions";
const DatasetsGetSchema = z.object({
name: z.string(),
name: z.string().transform((val) => decodeURIComponent(val)),
});
export default async function handler(
@@ -6,8 +6,8 @@ import { verifyAuthHeaderAndReturnScope } from "@/src/features/public-api/server
import { isPrismaException } from "@/src/utils/exceptions";
const DatasetRunsGetSchema = z.object({
name: z.string(),
runName: z.string(),
name: z.string().transform((val) => decodeURIComponent(val)),
runName: z.string().transform((val) => decodeURIComponent(val)),
});
export default async function handler(
@@ -108,6 +108,7 @@ export default async function handler(
SELECT
DATE_TRUNC('DAY', t.timestamp) "date",
count(distinct t.id)::integer count_traces,
count(distinct o.id)::integer count_observations,
SUM(o.calculated_total_cost)::DOUBLE PRECISION total_cost
FROM traces t
LEFT JOIN observations_view o ON o.project_id = t.project_id AND t.id = o.trace_id
@@ -122,6 +123,7 @@ export default async function handler(
SELECT
TO_CHAR(COALESCE(ds.date, daily_model_usage.date), 'YYYY-MM-DD') AS "date",
COALESCE(count_traces, 0) "countTraces",
COALESCE(count_observations, 0) "countObservations",
COALESCE(total_cost, 0) "totalCost",
COALESCE(daily_usage_json, '[]'::JSON) usage
FROM
+28 -23
View File
@@ -1,6 +1,6 @@
import { ChevronRightIcon } from "@heroicons/react/20/solid";
import { CommandLineIcon, RocketLaunchIcon } from "@heroicons/react/24/outline";
import { SiPython } from "react-icons/si";
import { SiOpenai, SiPython } from "react-icons/si";
import Header from "@/src/components/layouts/header";
import { ApiKeyList } from "@/src/features/public-api/components/ApiKeyList";
import { useRouter } from "next/router";
@@ -15,10 +15,6 @@ import { env } from "@/src/env.mjs";
import { Card } from "@tremor/react";
import { Button } from "@/src/components/ui/button";
import Link from "next/link";
import {
sendUserChatMessage,
showAgentChatMessage,
} from "@/src/features/support-chat/chat";
export default function SettingsPage() {
const router = useRouter();
@@ -31,7 +27,7 @@ export default function SettingsPage() {
<ApiKeyList projectId={projectId} />
<ProjectMembersTable projectId={projectId} />
<ProjectUsageChart projectId={projectId} />
<Beta />
<Integrations projectId={projectId} />
<Instructions />
<RenameProject projectId={projectId} />
<div className="space-y-3">
@@ -58,23 +54,36 @@ const instructionItems = [
icon: RocketLaunchIcon,
},
{
name: "Langchain integration",
name: "OpenAI SDK Integration",
description: "Trace your OpenAI API calls with a single line of code",
href: "https://langfuse.com/docs/integrations/openai",
icon: SiOpenai,
},
{
name: "Langchain Integration",
description:
"Trace your Langchain llm/chain/agent/... with a single line of code",
href: "https://langfuse.com/docs/langchain",
href: "https://langfuse.com/docs/integrations/langchain",
icon: Bird,
},
{
name: "LlamaIndex Integration",
description:
"Trace your Llamaindex RAG application by adding the global callback handler",
href: "https://langfuse.com/docs/integrations/llama-index",
icon: Code,
},
{
name: "Typescript SDK",
description: "npm install langfuse",
href: "https://langfuse.com/docs/sdk/typescript",
icon: CommandLineIcon,
icon: Code,
},
{
name: "Python SDK",
name: "Python SDK (Decorator)",
description: "pip install langfuse",
href: "https://langfuse.com/docs/sdk/python",
icon: SiPython,
icon: Code,
},
{
name: "API Reference (Swagger)",
@@ -123,12 +132,12 @@ function Instructions() {
);
}
const Beta = () => {
const Integrations = (props: { projectId: string }) => {
if (env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION === undefined) return null;
return (
<div>
<Header title="Early Access" level="h3" />
<Header title="Integrations" level="h3" />
<Card className="p-4 lg:w-1/2">
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
@@ -141,16 +150,12 @@ const Beta = () => {
Langfuse Events/Metrics available in your Posthog Dashboards.
</p>
<div className="flex items-center gap-2">
<Button
variant="secondary"
onClick={() => {
sendUserChatMessage(
"I am interested to join the PostHog Integration Beta",
);
showAgentChatMessage("We'll be in touch to get you set up!");
}}
>
Get Access
<Button variant="secondary" asChild>
<Link
href={`/project/${props.projectId}/settings/posthog-integration`}
>
Configure
</Link>
</Button>
<Button asChild variant="ghost">
<Link href="https://langfuse.com/docs/analytics/posthog">
@@ -0,0 +1,222 @@
import Header from "@/src/components/layouts/header";
import { Button } from "@/src/components/ui/button";
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/src/components/ui/form";
import { Input } from "@/src/components/ui/input";
import { PasswordInput } from "@/src/components/ui/password-input";
import { Switch } from "@/src/components/ui/switch";
import { env } from "@/src/env.mjs";
import { posthogIntegrationFormSchema } from "@/src/features/posthog-integration/types";
import { api } from "@/src/utils/api";
import { type RouterOutput } from "@/src/utils/types";
import { zodResolver } from "@hookform/resolvers/zod";
import { Card } from "@tremor/react";
import Link from "next/link";
import { useRouter } from "next/router";
import posthog from "posthog-js";
import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { type z } from "zod";
export default function PosthogIntegrationSettings() {
const router = useRouter();
const projectId = router.query.projectId as string;
const state = api.posthogIntegration.get.useQuery({ projectId });
if (env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION === undefined) return null;
return (
<div className="md:container">
<Header
title="PostHog Integration"
breadcrumb={[
{ name: "Settings", href: `/project/${projectId}/settings` },
]}
actionButtons={
<Button asChild variant="secondary">
<Link href="https://langfuse.com/docs/analytics/posthog">
Integration Docs
</Link>
</Button>
}
status={state.data?.enabled ? "active" : "inactive"}
/>
<p className="mb-4 text-sm text-gray-700">
We have teamed up with{" "}
<Link href="https://posthog.com" className="underline">
PostHog
</Link>{" "}
(OSS product analytics) to make Langfuse events/metrics available in
your Posthog Dashboards. While in Beta, this integration syncs metrics
on a daily schedule to PostHog. When first activated, it will sync all
historical data from the beginning of your project.
</p>
<div className="flex flex-col gap-10"></div>
{!state.isInitialLoading && (
<>
<Header level="h3" title="Configuration" />
<Card className="p-4">
<PostHogIntegrationSettings
state={state.data}
projectId={projectId}
/>
</Card>
</>
)}
{state.data?.enabled && (
<>
<Header level="h3" title="Status" className="mt-8" />
<p className="text-sm text-gray-700">
Data synced until:{" "}
{state.data?.lastSyncAt
? new Date(state.data.lastSyncAt).toLocaleString()
: "Never (pending)"}
</p>
<p className="mt-2 text-sm text-gray-700">
While in Beta, the sync is scheduled to run once a day.
</p>
</>
)}
</div>
);
}
const PostHogIntegrationSettings = ({
state,
projectId,
}: {
state?: RouterOutput["posthogIntegration"]["get"];
projectId: string;
}) => {
const posthogForm = useForm<z.infer<typeof posthogIntegrationFormSchema>>({
resolver: zodResolver(posthogIntegrationFormSchema),
defaultValues: {
posthogHostname: state?.posthogHostName ?? "",
posthogProjectApiKey: state?.posthogApiKey ?? "",
enabled: state?.enabled ?? false,
},
});
useEffect(() => {
posthogForm.reset({
posthogHostname: state?.posthogHostName ?? "",
posthogProjectApiKey: state?.posthogApiKey ?? "",
enabled: state?.enabled ?? false,
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [state]);
const utils = api.useUtils();
const mut = api.posthogIntegration.update.useMutation({
onSuccess: () => {
utils.posthogIntegration.invalidate();
},
});
const mutDelete = api.posthogIntegration.delete.useMutation({
onSuccess: () => {
utils.posthogIntegration.invalidate();
},
});
async function onSubmit(
values: z.infer<typeof posthogIntegrationFormSchema>,
) {
posthog.capture("integrations:posthog_form_submitted");
mut.mutate({
projectId,
...values,
});
console.log(values);
}
return (
<Form {...posthogForm}>
<form
className="space-y-3"
// eslint-disable-next-line @typescript-eslint/no-misused-promises
onSubmit={posthogForm.handleSubmit(onSubmit)}
>
<FormField
control={posthogForm.control}
name="posthogHostname"
render={({ field }) => (
<FormItem>
<FormLabel>Posthog Hostname</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormDescription>
US region: https://us.posthog.com; EU region:
https://eu.posthog.com
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={posthogForm.control}
name="posthogProjectApiKey"
render={({ field }) => (
<FormItem>
<FormLabel>Posthog Project API Key</FormLabel>
<FormControl>
<PasswordInput {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={posthogForm.control}
name="enabled"
render={({ field }) => (
<FormItem>
<FormLabel>Enabled</FormLabel>
<FormControl>
<Switch
id="posthog-integration-enabled"
checked={field.value}
onCheckedChange={() => {
field.onChange(!field.value);
}}
className="ml-4 mt-1"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</form>
<div className="mt-8 flex gap-2">
<Button
loading={mut.isLoading}
onClick={posthogForm.handleSubmit(onSubmit)}
>
Save
</Button>
<Button
variant="ghost"
loading={mutDelete.isLoading}
onClick={() => {
if (
confirm(
"Are you sure you want to reset the PostHog integration for this project?",
)
)
mutDelete.mutate({ projectId });
}}
>
Reset
</Button>
</div>
</Form>
);
};
+2
View File
@@ -15,6 +15,7 @@ import { sessionRouter } from "@/src/server/api/routers/sessions";
import { promptRouter } from "@/src/features/prompts/server/prompt-router";
import { modelRouter } from "@/src/server/api/routers/models";
import { evalRouter } from "@/src/features/evals/server/router";
import { posthogIntegrationRouter } from "@/src/features/posthog-integration/posthog-integration-router";
/**
* This is the primary router for your server.
@@ -38,6 +39,7 @@ export const appRouter = createTRPCRouter({
prompts: promptRouter,
models: modelRouter,
evals: evalRouter,
posthogIntegration: posthogIntegrationRouter,
});
// export type definition of API
+14 -12
View File
@@ -19,17 +19,19 @@ export const userRouter = createTRPCRouter({
all: protectedProjectProcedure
.input(UserAllOptions)
.query(async ({ input, ctx }) => {
const uniqueUsers = await ctx.prisma.trace.findMany({
where: {
projectId: input.projectId,
},
select: {
userId: true,
},
distinct: ["userId"],
});
const totalUsers = (
await ctx.prisma.$queryRaw<
Array<{
totalCount: number;
}>
>`
SELECT COUNT(DISTINCT t.user_id)::int AS "totalCount"
FROM traces t
WHERE t.project_id = ${input.projectId}
`
)[0].totalCount;
const topUsers = await ctx.prisma.$queryRaw<
const users = await ctx.prisma.$queryRaw<
Array<{
userId: string;
totalTraces: number;
@@ -52,8 +54,8 @@ export const userRouter = createTRPCRouter({
${input.limit} OFFSET ${input.page * input.limit};
`;
return {
totalUsers: uniqueUsers.length,
users: topUsers,
totalUsers,
users,
};
}),
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "worker",
"version": "2.30.0",
"version": "2.30.2",
"description": "",
"license": "MIT",
"main": "index.js",
+1 -1
View File
@@ -1 +1 @@
export const VERSION = "v2.30.0";
export const VERSION = "v2.30.2";