Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8584f93307 | ||
|
|
08e576f815 | ||
|
|
f64da44b20 | ||
|
|
c720a4d896 | ||
|
|
b59b61a5cf | ||
|
|
3f18e42fef | ||
|
|
31a9be6f1d |
+2
-8
@@ -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
@@ -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";
|
||||
|
||||
|
||||
@@ -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
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "langfuse",
|
||||
"version": "2.30.0",
|
||||
"version": "2.30.2",
|
||||
"author": "engineering@langfuse.com",
|
||||
"license": "MIT",
|
||||
"private": true,
|
||||
|
||||
@@ -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;
|
||||
|
||||
+16
@@ -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;
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
@@ -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";
|
||||
|
||||
@@ -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
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "web",
|
||||
"version": "2.30.0",
|
||||
"version": "2.30.2",
|
||||
"private": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
|
||||
@@ -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",
|
||||
}),
|
||||
]),
|
||||
});
|
||||
|
||||
@@ -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 @@
|
||||
export const VERSION = "v2.30.0";
|
||||
export const VERSION = "v2.30.2";
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "worker",
|
||||
"version": "2.30.0",
|
||||
"version": "2.30.2",
|
||||
"description": "",
|
||||
"license": "MIT",
|
||||
"main": "index.js",
|
||||
|
||||
@@ -1 +1 @@
|
||||
export const VERSION = "v2.30.0";
|
||||
export const VERSION = "v2.30.2";
|
||||
|
||||
Reference in New Issue
Block a user