mirror of
https://github.com/BillyOutlast/posthog.git
synced 2026-02-04 03:01:23 +01:00
chore: Refactor mcp to use global monorepo oxlint and prettier (#41233)
This commit is contained in:
41
.github/workflows/ci-mcp.yml
vendored
41
.github/workflows/ci-mcp.yml
vendored
@@ -26,47 +26,6 @@ jobs:
|
||||
- '.github/workflows/mcp-ci.yml'
|
||||
- '.github/workflows/mcp-publish.yml'
|
||||
|
||||
lint-and-format:
|
||||
name: Lint, Format, and Type Check
|
||||
runs-on: ubuntu-latest
|
||||
needs: changes
|
||||
if: needs.changes.outputs.mcp == 'true'
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Run linter
|
||||
run: cd products/mcp && pnpm run lint
|
||||
|
||||
- name: Run formatter
|
||||
run: cd products/mcp && pnpm run format
|
||||
|
||||
- name: Run type check
|
||||
run: cd products/mcp && pnpm run typecheck
|
||||
|
||||
- name: Check for changes
|
||||
run: |
|
||||
if [ -n "$(git status --porcelain)" ]; then
|
||||
echo "Code formatting or linting changes detected!"
|
||||
git diff
|
||||
exit 1
|
||||
fi
|
||||
|
||||
unit-tests:
|
||||
name: Unit Tests
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@@ -16,7 +16,10 @@
|
||||
"common/plugin_transpiler/dist",
|
||||
"common/hogvm/__tests__/__snapshots__",
|
||||
"common/hogvm/__tests__/__snapshots__/**",
|
||||
"frontend/src/queries/validators.js"
|
||||
"frontend/src/queries/validators.js",
|
||||
"products/mcp/**/generated.ts",
|
||||
"products/mcp/schema/tool-inputs.json",
|
||||
"products/mcp/python"
|
||||
],
|
||||
"rules": {
|
||||
"no-constant-condition": "off",
|
||||
|
||||
@@ -6,6 +6,7 @@ staticfiles
|
||||
.env
|
||||
*.code-workspace
|
||||
.mypy_cache
|
||||
.cache/
|
||||
*Type.ts
|
||||
.idea
|
||||
.yalc
|
||||
@@ -15,8 +16,7 @@ common/storybook/dist/
|
||||
dist/
|
||||
node_modules/
|
||||
pnpm-lock.yaml
|
||||
posthog/templates/email/*
|
||||
posthog/templates/**/*.html
|
||||
posthog/templates/**/*
|
||||
common/hogvm/typescript/src/stl/bytecode.ts
|
||||
common/hogvm/__tests__/__snapshots__/*
|
||||
rust/
|
||||
@@ -26,3 +26,10 @@ cli/tests/_cases/**/*
|
||||
frontend/src/products.tsx
|
||||
frontend/src/layout.html
|
||||
**/fixtures/**
|
||||
products/mcp/schema/**/*
|
||||
products/mcp/python/**/*
|
||||
products/mcp/**/generated.ts
|
||||
products/mcp/typescript/worker-configuration.d.ts
|
||||
frontend/src/taxonomy/core-filter-definitions-by-group.json
|
||||
frontend/dist/
|
||||
frontend/**/*LogicType.ts
|
||||
@@ -14,6 +14,7 @@
|
||||
"^@posthog.*$",
|
||||
"^lib/(.*)$|^scenes/(.*)$",
|
||||
"^~/(.*)$",
|
||||
"^@/(.*)$",
|
||||
"^public/(.*)$",
|
||||
"^products/(.*)$",
|
||||
"^storybook/(.*)$",
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
.cache/
|
||||
src/taxonomy/core-filter-definitions-by-group.json
|
||||
dist/
|
||||
*LogicType.ts
|
||||
@@ -37,8 +37,8 @@
|
||||
"schema:build:json": "ts-node ./bin/build-schema-json.mjs && prettier --write ./src/queries/schema.json && ts-node ./bin/build-validators.mjs && prettier --write ./src/queries/validators.js",
|
||||
"test": "SHARD_IDX=${SHARD_INDEX:-1}; SHARD_TOTAL=${SHARD_COUNT:-1}; echo $SHARD_IDX/$SHARD_TOTAL; pnpm build:products && jest --testPathPattern='(frontend/|products/|common/)' --runInBand --forceExit --shard=$SHARD_IDX/$SHARD_TOTAL",
|
||||
"jest": "pnpm build:products && jest",
|
||||
"prettier": "prettier --write \"../{products,frontend/src}/**/*.{js,mjs,ts,tsx,json,yaml,yml,css,scss}\"",
|
||||
"prettier:check": "prettier --check \"../{products,frontend/src}/**/*.{js,mjs,ts,tsx,json,yaml,yml,css,scss}\"",
|
||||
"prettier": "prettier --write \"../{products,frontend/src}/**/*.{js,mjs,ts,tsx,json,yaml,yml,css,scss}\" --ignore-path \"../.prettierignore\"",
|
||||
"prettier:check": "prettier --check \"../{products,frontend/src}/**/*.{js,mjs,ts,tsx,json,yaml,yml,css,scss}\" --ignore-path \"../.prettierignore\"",
|
||||
"typescript:check": "tsc --noEmit && echo \"No errors reported by tsc.\"",
|
||||
"lint": "pnpm lint:js && pnpm lint:css",
|
||||
"lint:js": "cd .. && oxlint --quiet",
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"$schema": "./node_modules/oxlint/configuration_schema.json",
|
||||
"files": {
|
||||
"ignore": ["**/generated.ts", "schema/tool-inputs.json", "python/**", ".mypy_cache/**"]
|
||||
},
|
||||
"rules": {
|
||||
"no-debugger": "off",
|
||||
"no-console": "off",
|
||||
"@typescript-eslint/explicit-function-return-type": "off",
|
||||
"jest/no-restricted-matchers": "off"
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
schema/tool-inputs.json
|
||||
python/**
|
||||
**/generated.ts
|
||||
node_modules/
|
||||
.mypy_cache/
|
||||
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"trailingComma": "es5",
|
||||
"tabWidth": 4,
|
||||
"semi": false,
|
||||
"singleQuote": true,
|
||||
"printWidth": 100
|
||||
}
|
||||
@@ -20,21 +20,21 @@ npx @posthog/wizard@latest mcp add
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"posthog": {
|
||||
"command": "npx",
|
||||
"args": [
|
||||
"-y",
|
||||
"mcp-remote@latest",
|
||||
"https://mcp.posthog.com/mcp", // You can replace this with https://mcp.posthog.com/sse if your client does not support Streamable HTTP
|
||||
"--header",
|
||||
"Authorization:${POSTHOG_AUTH_HEADER}"
|
||||
],
|
||||
"env": {
|
||||
"POSTHOG_AUTH_HEADER": "Bearer {INSERT_YOUR_PERSONAL_API_KEY_HERE}"
|
||||
}
|
||||
}
|
||||
"mcpServers": {
|
||||
"posthog": {
|
||||
"command": "npx",
|
||||
"args": [
|
||||
"-y",
|
||||
"mcp-remote@latest",
|
||||
"https://mcp.posthog.com/mcp", // You can replace this with https://mcp.posthog.com/sse if your client does not support Streamable HTTP
|
||||
"--header",
|
||||
"Authorization:${POSTHOG_AUTH_HEADER}"
|
||||
],
|
||||
"env": {
|
||||
"POSTHOG_AUTH_HEADER": "Bearer {INSERT_YOUR_PERSONAL_API_KEY_HERE}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -51,27 +51,27 @@ If you want to call MCP from Node (outside an IDE), use the Model Context Protoc
|
||||
import { Client } from '@modelcontextprotocol/sdk/client/index.js'
|
||||
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'
|
||||
import { ListToolsResultSchema } from '@modelcontextprotocol/sdk/types.js'
|
||||
import { URL } from 'node:url'
|
||||
import { mkdirSync, writeFileSync } from 'node:fs'
|
||||
import { join } from 'node:path'
|
||||
import { URL } from 'node:url'
|
||||
|
||||
const AUTH = process.env.POSTHOG_AUTH_HEADER // "Bearer phx_…"
|
||||
const MCP_URL = process.env.MCP_URL || 'https://mcp.posthog.com/mcp'
|
||||
|
||||
if (!AUTH?.startsWith('Bearer ')) {
|
||||
console.error('Set POSTHOG_AUTH_HEADER="Bearer phx_..."')
|
||||
process.exit(1)
|
||||
console.error('Set POSTHOG_AUTH_HEADER="Bearer phx_..."')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const transport = new StreamableHTTPClientTransport(new URL(MCP_URL), {
|
||||
requestInit: {
|
||||
headers: {
|
||||
Authorization: AUTH,
|
||||
// Required for Streamable HTTP (JSON + SSE)
|
||||
Accept: 'application/json, text/event-stream',
|
||||
},
|
||||
requestInit: {
|
||||
headers: {
|
||||
Authorization: AUTH,
|
||||
// Required for Streamable HTTP (JSON + SSE)
|
||||
Accept: 'application/json, text/event-stream',
|
||||
},
|
||||
serverInfo: { name: 'example-node-client', version: '0.0.1' },
|
||||
},
|
||||
serverInfo: { name: 'example-node-client', version: '0.0.1' },
|
||||
})
|
||||
|
||||
const client = new Client({ name: 'example-node-client', version: '0.0.1' })
|
||||
@@ -115,26 +115,26 @@ docker build -t posthog-mcp .
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"posthog": {
|
||||
"type": "stdio",
|
||||
"command": "docker",
|
||||
"args": [
|
||||
"run",
|
||||
"-i",
|
||||
"--rm",
|
||||
"--env",
|
||||
"POSTHOG_AUTH_HEADER=${POSTHOG_AUTH_HEADER}",
|
||||
"--env",
|
||||
"POSTHOG_REMOTE_MCP_URL=${POSTHOG_REMOTE_MCP_URL:-https://mcp.posthog.com/mcp}",
|
||||
"posthog-mcp"
|
||||
],
|
||||
"env": {
|
||||
"POSTHOG_AUTH_HEADER": "Bearer {INSERT_YOUR_PERSONAL_API_KEY_HERE}",
|
||||
"POSTHOG_REMOTE_MCP_URL": "https://mcp.posthog.com/mcp"
|
||||
}
|
||||
}
|
||||
"mcpServers": {
|
||||
"posthog": {
|
||||
"type": "stdio",
|
||||
"command": "docker",
|
||||
"args": [
|
||||
"run",
|
||||
"-i",
|
||||
"--rm",
|
||||
"--env",
|
||||
"POSTHOG_AUTH_HEADER=${POSTHOG_AUTH_HEADER}",
|
||||
"--env",
|
||||
"POSTHOG_REMOTE_MCP_URL=${POSTHOG_REMOTE_MCP_URL:-https://mcp.posthog.com/mcp}",
|
||||
"posthog-mcp"
|
||||
],
|
||||
"env": {
|
||||
"POSTHOG_AUTH_HEADER": "Bearer {INSERT_YOUR_PERSONAL_API_KEY_HERE}",
|
||||
"POSTHOG_REMOTE_MCP_URL": "https://mcp.posthog.com/mcp"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -239,14 +239,5 @@ Alternatively, you can use the following configuration in the MCP Inspector:
|
||||
|
||||
Use transport type `STDIO`.
|
||||
|
||||
**Command:**
|
||||
|
||||
```bash
|
||||
npx
|
||||
```
|
||||
|
||||
**Arguments:**
|
||||
|
||||
```bash
|
||||
-y mcp-remote@latest http://localhost:8787/mcp --header "Authorization: Bearer {INSERT_YOUR_PERSONAL_API_KEY_HERE}"
|
||||
```
|
||||
- **Command**: `npx`
|
||||
- **Arguments**: `-y mcp-remote@latest http://localhost:8787/mcp --header "Authorization: Bearer {INSERT_YOUR_PERSONAL_API_KEY_HERE}"`
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import { openai } from '@ai-sdk/openai'
|
||||
import { PostHogAgentToolkit } from '@posthog/agent-toolkit/integrations/ai-sdk'
|
||||
import { generateText, stepCountIs } from 'ai'
|
||||
import 'dotenv/config'
|
||||
|
||||
async function analyzeProductUsage() {
|
||||
import { PostHogAgentToolkit } from '@posthog/agent-toolkit/integrations/ai-sdk'
|
||||
|
||||
async function analyzeProductUsage(): Promise<void> {
|
||||
const agentToolkit = new PostHogAgentToolkit({
|
||||
posthogPersonalApiKey: process.env.POSTHOG_PERSONAL_API_KEY!,
|
||||
posthogApiBaseUrl: process.env.POSTHOG_API_BASE_URL || 'https://us.posthog.com',
|
||||
})
|
||||
|
||||
const result = await generateText({
|
||||
await generateText({
|
||||
model: openai('gpt-5-mini'),
|
||||
tools: await agentToolkit.getTools(),
|
||||
stopWhen: stepCountIs(30),
|
||||
@@ -24,7 +25,7 @@ async function analyzeProductUsage() {
|
||||
})
|
||||
}
|
||||
|
||||
async function main() {
|
||||
async function main(): Promise<void> {
|
||||
try {
|
||||
await analyzeProductUsage()
|
||||
} catch (error) {
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { ChatPromptTemplate, MessagesPlaceholder } from '@langchain/core/prompts'
|
||||
import { ChatOpenAI } from '@langchain/openai'
|
||||
import { PostHogAgentToolkit } from '@posthog/agent-toolkit/integrations/langchain'
|
||||
import { AgentExecutor, createToolCallingAgent } from 'langchain/agents'
|
||||
import 'dotenv/config'
|
||||
import { AgentExecutor, createToolCallingAgent } from 'langchain/agents'
|
||||
|
||||
async function analyzeProductUsage() {
|
||||
import { PostHogAgentToolkit } from '@posthog/agent-toolkit/integrations/langchain'
|
||||
|
||||
async function analyzeProductUsage(): Promise<void> {
|
||||
const agentToolkit = new PostHogAgentToolkit({
|
||||
posthogPersonalApiKey: process.env.POSTHOG_PERSONAL_API_KEY!,
|
||||
posthogApiBaseUrl: process.env.POSTHOG_API_BASE_URL || 'https://us.posthog.com',
|
||||
@@ -38,7 +39,7 @@ async function analyzeProductUsage() {
|
||||
maxIterations: 5,
|
||||
})
|
||||
|
||||
const result = await agentExecutor.invoke({
|
||||
await agentExecutor.invoke({
|
||||
input: `Please analyze our product usage:
|
||||
|
||||
1. Get all available insights (limit 100) and pick the 5 most relevant ones
|
||||
@@ -49,7 +50,7 @@ async function analyzeProductUsage() {
|
||||
})
|
||||
}
|
||||
|
||||
async function main() {
|
||||
async function main(): Promise<void> {
|
||||
try {
|
||||
await analyzeProductUsage()
|
||||
} catch (error) {
|
||||
|
||||
@@ -19,8 +19,8 @@ cp .env.example .env
|
||||
```
|
||||
|
||||
3. Update your `.env` file with:
|
||||
- `POSTHOG_PERSONAL_API_KEY`: Your PostHog personal API key
|
||||
- `OPENAI_API_KEY`: Your OpenAI API key
|
||||
- `POSTHOG_PERSONAL_API_KEY`: Your PostHog personal API key
|
||||
- `OPENAI_API_KEY`: Your OpenAI API key
|
||||
|
||||
## Usage
|
||||
|
||||
@@ -38,9 +38,9 @@ The example will:
|
||||
2. Load all available PostHog tools from the MCP server
|
||||
3. Create a LangChain agent with access to PostHog data
|
||||
4. Analyze product usage by:
|
||||
- Getting available insights
|
||||
- Querying data for the most relevant ones
|
||||
- Summarizing key findings
|
||||
- Getting available insights
|
||||
- Querying data for the most relevant ones
|
||||
- Summarizing key findings
|
||||
|
||||
## Available Tools
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "posthog-mcp-monorepo",
|
||||
"name": "mcp",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
@@ -11,13 +11,7 @@
|
||||
"test:integration": "cd typescript && pnpm test:integration",
|
||||
"schema:build:python": "bash python/scripts/generate-pydantic-models.sh",
|
||||
"schema:build": "pnpm run schema:build:json && pnpm run schema:build:python",
|
||||
"format": "prettier --write . && oxlint --fix --fix-suggestions --quiet .",
|
||||
"lint": "prettier --check . && oxlint .",
|
||||
"format:python": "cd python && uv run ruff format .",
|
||||
"lint:python": "cd python && uv run ruff check --fix .",
|
||||
"test:python": "cd python && uv run pytest tests/ -v",
|
||||
"typecheck": "cd typescript && pnpm typecheck",
|
||||
"typecheck:python": "cd python && uvx ty check",
|
||||
"docker:build": "docker build -t posthog-mcp .",
|
||||
"docker:run": "docker run -i --rm --env POSTHOG_AUTH_HEADER=${POSTHOG_AUTH_HEADER} --env POSTHOG_REMOTE_MCP_URL=${POSTHOG_REMOTE_MCP_URL:-https://mcp.posthog.com/mcp} posthog-mcp",
|
||||
"docker:inspector": "npx @modelcontextprotocol/inspector docker run -i --rm --env POSTHOG_AUTH_HEADER=${POSTHOG_AUTH_HEADER} --env POSTHOG_REMOTE_MCP_URL=${POSTHOG_REMOTE_MCP_URL:-https://mcp.posthog.com/mcp} posthog-mcp"
|
||||
|
||||
@@ -21,14 +21,15 @@ import { generateText } from 'ai'
|
||||
import { PostHogAgentToolkit } from '@posthog/agent-toolkit/integrations/ai-sdk'
|
||||
|
||||
const toolkit = new PostHogAgentToolkit({
|
||||
posthogPersonalApiKey: process.env.POSTHOG_PERSONAL_API_KEY!,
|
||||
posthogApiBaseUrl: 'https://us.posthog.com', // or https://eu.posthog.com if you are hosting in the EU
|
||||
posthogPersonalApiKey: process.env.POSTHOG_PERSONAL_API_KEY!,
|
||||
posthogApiBaseUrl: 'https://us.posthog.com', // or https://eu.posthog.com if you are hosting in the EU
|
||||
})
|
||||
|
||||
const result = await generateText({
|
||||
model: openai('gpt-4'),
|
||||
tools: await toolkit.getTools(),
|
||||
prompt: 'Analyze our product usage by getting the top 5 most interesting insights and summarising the data from them.',
|
||||
model: openai('gpt-4'),
|
||||
tools: await toolkit.getTools(),
|
||||
prompt:
|
||||
'Analyze our product usage by getting the top 5 most interesting insights and summarising the data from them.',
|
||||
})
|
||||
```
|
||||
|
||||
@@ -44,24 +45,24 @@ import { AgentExecutor, createToolCallingAgent } from 'langchain/agents'
|
||||
import { PostHogAgentToolkit } from '@posthog/agent-toolkit/integrations/langchain'
|
||||
|
||||
const toolkit = new PostHogAgentToolkit({
|
||||
posthogPersonalApiKey: process.env.POSTHOG_PERSONAL_API_KEY!,
|
||||
posthogApiBaseUrl: 'https://us.posthog.com', // or https://eu.posthog.com if you are hosting in the EU
|
||||
posthogPersonalApiKey: process.env.POSTHOG_PERSONAL_API_KEY!,
|
||||
posthogApiBaseUrl: 'https://us.posthog.com', // or https://eu.posthog.com if you are hosting in the EU
|
||||
})
|
||||
|
||||
const tools = await toolkit.getTools()
|
||||
const llm = new ChatOpenAI({ model: 'gpt-4' })
|
||||
|
||||
const prompt = ChatPromptTemplate.fromMessages([
|
||||
['system', 'You are a data analyst with access to PostHog analytics'],
|
||||
['human', '{input}'],
|
||||
new MessagesPlaceholder('agent_scratchpad'),
|
||||
['system', 'You are a data analyst with access to PostHog analytics'],
|
||||
['human', '{input}'],
|
||||
new MessagesPlaceholder('agent_scratchpad'),
|
||||
])
|
||||
|
||||
const agent = createToolCallingAgent({ llm, tools, prompt })
|
||||
const executor = new AgentExecutor({ agent, tools })
|
||||
|
||||
const result = await executor.invoke({
|
||||
input: 'Analyze our product usage by getting the top 5 most interesting insights and summarising the data from them.',
|
||||
input: 'Analyze our product usage by getting the top 5 most interesting insights and summarising the data from them.',
|
||||
})
|
||||
```
|
||||
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
#!/usr/bin/env tsx
|
||||
|
||||
// Generates JSON schema from Zod tool-inputs schemas for Python Pydantic schema generation
|
||||
|
||||
import * as fs from 'node:fs'
|
||||
import * as path from 'node:path'
|
||||
import { zodToJsonSchema } from 'zod-to-json-schema'
|
||||
|
||||
import * as schemas from '../src/schema/tool-inputs'
|
||||
|
||||
const outputPath = path.join(__dirname, '../../schema/tool-inputs.json')
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
#!/usr/bin/env tsx
|
||||
|
||||
import { execSync } from 'node:child_process'
|
||||
import * as fs from 'node:fs'
|
||||
|
||||
@@ -37,7 +36,7 @@ function generateClient() {
|
||||
}
|
||||
}
|
||||
|
||||
function cleanup() {
|
||||
function cleanup(): void {
|
||||
try {
|
||||
if (fs.existsSync(TEMP_SCHEMA_PATH)) {
|
||||
fs.unlinkSync(TEMP_SCHEMA_PATH)
|
||||
@@ -47,7 +46,7 @@ function cleanup() {
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
async function main(): Promise<void> {
|
||||
const schemaFetched = await fetchSchema()
|
||||
if (!schemaFetched) {
|
||||
process.exit(1)
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { z } from 'zod'
|
||||
|
||||
import { ErrorCode } from '@/lib/errors'
|
||||
import { withPagination } from '@/lib/utils/api'
|
||||
import { getSearchParamsFromRecord } from '@/lib/utils/helper-functions'
|
||||
@@ -51,7 +53,7 @@ import { type Organization, OrganizationSchema } from '@/schema/orgs'
|
||||
import { type Project, ProjectSchema } from '@/schema/projects'
|
||||
import type { ExperimentCreateSchema } from '@/schema/tool-inputs'
|
||||
import { isShortId } from '@/tools/insights/utils'
|
||||
import { z } from 'zod'
|
||||
|
||||
import type {
|
||||
CreateSurveyInput,
|
||||
GetSurveySpecificStatsInput,
|
||||
@@ -81,6 +83,9 @@ export interface ApiConfig {
|
||||
apiToken: string
|
||||
baseUrl: string
|
||||
}
|
||||
|
||||
type Endpoint = Record<string, any>
|
||||
|
||||
export class ApiClient {
|
||||
private config: ApiConfig
|
||||
private baseUrl: string
|
||||
@@ -93,14 +98,15 @@ export class ApiClient {
|
||||
|
||||
this.generated = createApiClient(buildApiFetcher(this.config), this.baseUrl)
|
||||
}
|
||||
private buildHeaders() {
|
||||
|
||||
private buildHeaders(): Record<string, string> {
|
||||
return {
|
||||
Authorization: `Bearer ${this.config.apiToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
}
|
||||
|
||||
getProjectBaseUrl(projectId: string) {
|
||||
getProjectBaseUrl(projectId: string): string {
|
||||
if (projectId === '@current') {
|
||||
return this.baseUrl
|
||||
}
|
||||
@@ -108,11 +114,7 @@ export class ApiClient {
|
||||
return `${this.baseUrl}/project/${projectId}`
|
||||
}
|
||||
|
||||
private async fetchWithSchema<T>(
|
||||
url: string,
|
||||
schema: z.ZodType<T>,
|
||||
options?: RequestInit
|
||||
): Promise<Result<T>> {
|
||||
private async fetchWithSchema<T>(url: string, schema: z.ZodType<T>, options?: RequestInit): Promise<Result<T>> {
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
...options,
|
||||
@@ -158,17 +160,14 @@ export class ApiClient {
|
||||
}
|
||||
}
|
||||
|
||||
organizations() {
|
||||
organizations(): Endpoint {
|
||||
return {
|
||||
list: async (): Promise<Result<Organization[]>> => {
|
||||
const responseSchema = z.object({
|
||||
results: z.array(OrganizationSchema),
|
||||
})
|
||||
|
||||
const result = await this.fetchWithSchema(
|
||||
`${this.baseUrl}/api/organizations/`,
|
||||
responseSchema
|
||||
)
|
||||
const result = await this.fetchWithSchema(`${this.baseUrl}/api/organizations/`, responseSchema)
|
||||
|
||||
if (result.success) {
|
||||
return { success: true, data: result.data.results }
|
||||
@@ -177,10 +176,7 @@ export class ApiClient {
|
||||
},
|
||||
|
||||
get: async ({ orgId }: { orgId: string }): Promise<Result<Organization>> => {
|
||||
return this.fetchWithSchema(
|
||||
`${this.baseUrl}/api/organizations/${orgId}/`,
|
||||
OrganizationSchema
|
||||
)
|
||||
return this.fetchWithSchema(`${this.baseUrl}/api/organizations/${orgId}/`, OrganizationSchema)
|
||||
},
|
||||
|
||||
projects: ({ orgId }: { orgId: string }) => {
|
||||
@@ -205,7 +201,7 @@ export class ApiClient {
|
||||
}
|
||||
}
|
||||
|
||||
apiKeys() {
|
||||
apiKeys(): Endpoint {
|
||||
return {
|
||||
current: async (): Promise<Result<ApiRedactedPersonalApiKey>> => {
|
||||
return this.fetchWithSchema(
|
||||
@@ -216,13 +212,10 @@ export class ApiClient {
|
||||
}
|
||||
}
|
||||
|
||||
projects() {
|
||||
projects(): Endpoint {
|
||||
return {
|
||||
get: async ({ projectId }: { projectId: string }): Promise<Result<Project>> => {
|
||||
return this.fetchWithSchema(
|
||||
`${this.baseUrl}/api/projects/${projectId}/`,
|
||||
ProjectSchema
|
||||
)
|
||||
return this.fetchWithSchema(`${this.baseUrl}/api/projects/${projectId}/`, ProjectSchema)
|
||||
},
|
||||
|
||||
propertyDefinitions: async ({
|
||||
@@ -268,9 +261,7 @@ export class ApiClient {
|
||||
ApiPropertyDefinitionSchema
|
||||
)
|
||||
|
||||
const propertyDefinitionsWithoutHidden = propertyDefinitions.filter(
|
||||
(def) => !def.hidden
|
||||
)
|
||||
const propertyDefinitionsWithoutHidden = propertyDefinitions.filter((def) => !def.hidden)
|
||||
|
||||
return { success: true, data: propertyDefinitionsWithoutHidden }
|
||||
} catch (error) {
|
||||
@@ -304,7 +295,7 @@ export class ApiClient {
|
||||
}
|
||||
}
|
||||
|
||||
experiments({ projectId }: { projectId: string }) {
|
||||
experiments({ projectId }: { projectId: string }): Endpoint {
|
||||
return {
|
||||
list: async (): Promise<Result<Experiment[]>> => {
|
||||
try {
|
||||
@@ -320,11 +311,7 @@ export class ApiClient {
|
||||
}
|
||||
},
|
||||
|
||||
get: async ({
|
||||
experimentId,
|
||||
}: {
|
||||
experimentId: number
|
||||
}): Promise<Result<Experiment>> => {
|
||||
get: async ({ experimentId }: { experimentId: number }): Promise<Result<Experiment>> => {
|
||||
return this.fetchWithSchema(
|
||||
`${this.baseUrl}/api/projects/${projectId}/experiments/${experimentId}/`,
|
||||
ExperimentSchema
|
||||
@@ -474,10 +461,7 @@ export class ApiClient {
|
||||
const sharedSecondaryMetrics = (experiment.saved_metrics || [])
|
||||
.filter(({ metadata }) => metadata.type === 'secondary')
|
||||
.map(({ query }) => query)
|
||||
const allSecondaryMetrics = [
|
||||
...(experiment.metrics_secondary || []),
|
||||
...sharedSecondaryMetrics,
|
||||
]
|
||||
const allSecondaryMetrics = [...(experiment.metrics_secondary || []), ...sharedSecondaryMetrics]
|
||||
|
||||
// Execute queries for primary metrics
|
||||
const primaryResults = await Promise.all(
|
||||
@@ -504,7 +488,7 @@ export class ApiClient {
|
||||
)
|
||||
|
||||
return result.success ? result.data : null
|
||||
} catch (error) {
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
})
|
||||
@@ -535,7 +519,7 @@ export class ApiClient {
|
||||
)
|
||||
|
||||
return result.success ? result.data : null
|
||||
} catch (error) {
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
})
|
||||
@@ -552,9 +536,7 @@ export class ApiClient {
|
||||
}
|
||||
},
|
||||
|
||||
create: async (
|
||||
experimentData: z.infer<typeof ExperimentCreateSchema>
|
||||
): Promise<Result<Experiment>> => {
|
||||
create: async (experimentData: z.infer<typeof ExperimentCreateSchema>): Promise<Result<Experiment>> => {
|
||||
// Transform agent input to API payload
|
||||
const createBody = ExperimentCreatePayloadSchema.parse(experimentData)
|
||||
|
||||
@@ -624,11 +606,9 @@ export class ApiClient {
|
||||
}
|
||||
}
|
||||
|
||||
featureFlags({ projectId }: { projectId: string }) {
|
||||
featureFlags({ projectId }: { projectId: string }): Endpoint {
|
||||
return {
|
||||
list: async (): Promise<
|
||||
Result<Array<{ id: number; key: string; name: string; active: boolean }>>
|
||||
> => {
|
||||
list: async (): Promise<Result<Array<{ id: number; key: string; name: string; active: boolean }>>> => {
|
||||
try {
|
||||
const schema = FeatureFlagSchema.pick({
|
||||
id: true,
|
||||
@@ -680,9 +660,7 @@ export class ApiClient {
|
||||
key,
|
||||
}: {
|
||||
key: string
|
||||
}): Promise<
|
||||
Result<{ id: number; key: string; name: string; active: boolean } | undefined>
|
||||
> => {
|
||||
}): Promise<Result<{ id: number; key: string; name: string; active: boolean } | undefined>> => {
|
||||
const listResult = await this.featureFlags({ projectId }).list()
|
||||
|
||||
if (!listResult.success) {
|
||||
@@ -768,20 +746,13 @@ export class ApiClient {
|
||||
)
|
||||
},
|
||||
|
||||
delete: async ({
|
||||
flagId,
|
||||
}: {
|
||||
flagId: number
|
||||
}): Promise<Result<{ success: boolean; message: string }>> => {
|
||||
delete: async ({ flagId }: { flagId: number }): Promise<Result<{ success: boolean; message: string }>> => {
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${this.baseUrl}/api/projects/${projectId}/feature_flags/${flagId}/`,
|
||||
{
|
||||
method: 'PATCH',
|
||||
headers: this.buildHeaders(),
|
||||
body: JSON.stringify({ deleted: true }),
|
||||
}
|
||||
)
|
||||
const response = await fetch(`${this.baseUrl}/api/projects/${projectId}/feature_flags/${flagId}/`, {
|
||||
method: 'PATCH',
|
||||
headers: this.buildHeaders(),
|
||||
body: JSON.stringify({ deleted: true }),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to delete feature flag: ${response.statusText}`)
|
||||
@@ -801,26 +772,21 @@ export class ApiClient {
|
||||
}
|
||||
}
|
||||
|
||||
insights({ projectId }: { projectId: string }) {
|
||||
insights({ projectId }: { projectId: string }): Endpoint {
|
||||
return {
|
||||
list: async ({ params }: { params?: ListInsightsData } = {}): Promise<
|
||||
Result<Array<Schemas.Insight>>
|
||||
> => {
|
||||
list: async ({ params }: { params?: ListInsightsData } = {}): Promise<Result<Array<Schemas.Insight>>> => {
|
||||
try {
|
||||
const response = await this.generated.get(
|
||||
'/api/projects/{project_id}/insights/',
|
||||
{
|
||||
path: { project_id: projectId },
|
||||
query: params
|
||||
? {
|
||||
limit: params.limit,
|
||||
offset: params.offset,
|
||||
//@ts-expect-error search is not implemented as a query parameter
|
||||
search: params.search,
|
||||
}
|
||||
: {},
|
||||
}
|
||||
)
|
||||
const response = await this.generated.get('/api/projects/{project_id}/insights/', {
|
||||
path: { project_id: projectId },
|
||||
query: params
|
||||
? {
|
||||
limit: params.limit,
|
||||
offset: params.offset,
|
||||
//@ts-expect-error search is not implemented as a query parameter
|
||||
search: params.search,
|
||||
}
|
||||
: {},
|
||||
})
|
||||
|
||||
return { success: true, data: response.results }
|
||||
} catch (error) {
|
||||
@@ -828,11 +794,7 @@ export class ApiClient {
|
||||
}
|
||||
},
|
||||
|
||||
create: async ({
|
||||
data,
|
||||
}: {
|
||||
data: CreateInsightInput
|
||||
}): Promise<Result<SimpleInsight>> => {
|
||||
create: async ({ data }: { data: CreateInsightInput }): Promise<Result<SimpleInsight>> => {
|
||||
const validatedInput = CreateInsightInputSchema.parse(data)
|
||||
|
||||
return this.fetchWithSchema(
|
||||
@@ -881,13 +843,7 @@ export class ApiClient {
|
||||
)
|
||||
},
|
||||
|
||||
update: async ({
|
||||
insightId,
|
||||
data,
|
||||
}: {
|
||||
insightId: number
|
||||
data: any
|
||||
}): Promise<Result<SimpleInsight>> => {
|
||||
update: async ({ insightId, data }: { insightId: number; data: any }): Promise<Result<SimpleInsight>> => {
|
||||
return this.fetchWithSchema(
|
||||
`${this.baseUrl}/api/projects/${projectId}/insights/${insightId}/`,
|
||||
SimpleInsightSchema,
|
||||
@@ -904,14 +860,11 @@ export class ApiClient {
|
||||
insightId: number
|
||||
}): Promise<Result<{ success: boolean; message: string }>> => {
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${this.baseUrl}/api/projects/${projectId}/insights/${insightId}/`,
|
||||
{
|
||||
method: 'PATCH',
|
||||
headers: this.buildHeaders(),
|
||||
body: JSON.stringify({ deleted: true }),
|
||||
}
|
||||
)
|
||||
const response = await fetch(`${this.baseUrl}/api/projects/${projectId}/insights/${insightId}/`, {
|
||||
method: 'PATCH',
|
||||
headers: this.buildHeaders(),
|
||||
body: JSON.stringify({ deleted: true }),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to delete insight: ${response.statusText}`)
|
||||
@@ -976,7 +929,7 @@ export class ApiClient {
|
||||
}
|
||||
}
|
||||
|
||||
dashboards({ projectId }: { projectId: string }) {
|
||||
dashboards({ projectId }: { projectId: string }): Endpoint {
|
||||
return {
|
||||
list: async ({ params }: { params?: ListDashboardsData } = {}): Promise<
|
||||
Result<
|
||||
@@ -1021,22 +974,14 @@ export class ApiClient {
|
||||
return result
|
||||
},
|
||||
|
||||
get: async ({
|
||||
dashboardId,
|
||||
}: {
|
||||
dashboardId: number
|
||||
}): Promise<Result<SimpleDashboard>> => {
|
||||
get: async ({ dashboardId }: { dashboardId: number }): Promise<Result<SimpleDashboard>> => {
|
||||
return this.fetchWithSchema(
|
||||
`${this.baseUrl}/api/projects/${projectId}/dashboards/${dashboardId}/`,
|
||||
SimpleDashboardSchema
|
||||
)
|
||||
},
|
||||
|
||||
create: async ({
|
||||
data,
|
||||
}: {
|
||||
data: CreateDashboardInput
|
||||
}): Promise<Result<{ id: number; name: string }>> => {
|
||||
create: async ({ data }: { data: CreateDashboardInput }): Promise<Result<{ id: number; name: string }>> => {
|
||||
const validatedInput = CreateDashboardInputSchema.parse(data)
|
||||
|
||||
const createResponseSchema = z.object({
|
||||
@@ -1124,36 +1069,25 @@ export class ApiClient {
|
||||
}
|
||||
}
|
||||
|
||||
query({ projectId }: { projectId: string }) {
|
||||
query({ projectId }: { projectId: string }): Endpoint {
|
||||
return {
|
||||
execute: async ({
|
||||
queryBody,
|
||||
}: {
|
||||
queryBody: any
|
||||
}): Promise<Result<{ results: any[] }>> => {
|
||||
execute: async ({ queryBody }: { queryBody: any }): Promise<Result<{ results: any[] }>> => {
|
||||
const responseSchema = z.object({
|
||||
results: z.array(z.any()),
|
||||
})
|
||||
|
||||
return this.fetchWithSchema(
|
||||
`${this.baseUrl}/api/environments/${projectId}/query/`,
|
||||
responseSchema,
|
||||
{
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ query: queryBody }),
|
||||
}
|
||||
)
|
||||
return this.fetchWithSchema(`${this.baseUrl}/api/environments/${projectId}/query/`, responseSchema, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ query: queryBody }),
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
users() {
|
||||
users(): Endpoint {
|
||||
return {
|
||||
me: async (): Promise<Result<ApiUser>> => {
|
||||
const result = await this.fetchWithSchema(
|
||||
`${this.baseUrl}/api/users/@me/`,
|
||||
ApiUserSchema
|
||||
)
|
||||
const result = await this.fetchWithSchema(`${this.baseUrl}/api/users/@me/`, ApiUserSchema)
|
||||
|
||||
if (!result.success) {
|
||||
return result
|
||||
@@ -1167,7 +1101,7 @@ export class ApiClient {
|
||||
}
|
||||
}
|
||||
|
||||
surveys({ projectId }: { projectId: string }) {
|
||||
surveys({ projectId }: { projectId: string }): Endpoint {
|
||||
return {
|
||||
list: async ({ params }: { params?: ListSurveysInput } = {}): Promise<
|
||||
Result<Array<SurveyListItemOutput>>
|
||||
@@ -1207,21 +1141,13 @@ export class ApiClient {
|
||||
)
|
||||
},
|
||||
|
||||
create: async ({
|
||||
data,
|
||||
}: {
|
||||
data: CreateSurveyInput
|
||||
}): Promise<Result<SurveyOutput>> => {
|
||||
create: async ({ data }: { data: CreateSurveyInput }): Promise<Result<SurveyOutput>> => {
|
||||
const validatedInput = CreateSurveyInputSchema.parse(data)
|
||||
|
||||
return this.fetchWithSchema(
|
||||
`${this.baseUrl}/api/projects/${projectId}/surveys/`,
|
||||
SurveyOutputSchema,
|
||||
{
|
||||
method: 'POST',
|
||||
body: JSON.stringify(validatedInput),
|
||||
}
|
||||
)
|
||||
return this.fetchWithSchema(`${this.baseUrl}/api/projects/${projectId}/surveys/`, SurveyOutputSchema, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(validatedInput),
|
||||
})
|
||||
},
|
||||
|
||||
update: async ({
|
||||
@@ -1266,9 +1192,7 @@ export class ApiClient {
|
||||
)
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`Failed to ${softDelete ? 'archive' : 'delete'} survey: ${response.statusText}`
|
||||
)
|
||||
throw new Error(`Failed to ${softDelete ? 'archive' : 'delete'} survey: ${response.statusText}`)
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -1295,9 +1219,7 @@ export class ApiClient {
|
||||
return this.fetchWithSchema(url, SurveyResponseStatsOutputSchema)
|
||||
},
|
||||
|
||||
stats: async (
|
||||
params: GetSurveySpecificStatsInput
|
||||
): Promise<Result<SurveyResponseStatsOutput>> => {
|
||||
stats: async (params: GetSurveySpecificStatsInput): Promise<Result<SurveyResponseStatsOutput>> => {
|
||||
const validatedParams = GetSurveySpecificStatsInputSchema.parse(params)
|
||||
|
||||
const searchParams = getSearchParamsFromRecord(validatedParams)
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import type { ApiConfig } from './client'
|
||||
import type { createApiClient } from './generated'
|
||||
|
||||
export const buildApiFetcher: (config: ApiConfig) => Parameters<typeof createApiClient>[0] = (
|
||||
config
|
||||
) => {
|
||||
export const buildApiFetcher: (config: ApiConfig) => Parameters<typeof createApiClient>[0] = (config) => {
|
||||
return {
|
||||
fetch: async (input) => {
|
||||
const headers = new Headers()
|
||||
@@ -41,9 +39,7 @@ export const buildApiFetcher: (config: ApiConfig) => Parameters<typeof createApi
|
||||
|
||||
if (!response.ok) {
|
||||
const errorResponse = await response.json()
|
||||
throw new Error(
|
||||
`Failed request: [${response.status}] ${JSON.stringify(errorResponse)}`
|
||||
)
|
||||
throw new Error(`Failed request: [${response.status}] ${JSON.stringify(errorResponse)}`)
|
||||
}
|
||||
|
||||
return response
|
||||
|
||||
@@ -31,12 +31,7 @@ export async function docsSearch(apiKey: string, userQuery: string): Promise<str
|
||||
|
||||
const data = (await response.json()) as InkeepResponse
|
||||
|
||||
if (
|
||||
data.choices &&
|
||||
data.choices.length > 0 &&
|
||||
data.choices[0]?.message &&
|
||||
data.choices[0].message.content
|
||||
) {
|
||||
if (data.choices && data.choices.length > 0 && data.choices[0]?.message && data.choices[0].message.content) {
|
||||
return data.choices[0].message.content
|
||||
}
|
||||
console.error('Inkeep API response format unexpected:', data)
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import { type Tool as VercelTool, tool } from 'ai'
|
||||
import type { z } from 'zod'
|
||||
|
||||
import { ApiClient } from '@/api/client'
|
||||
import { SessionManager } from '@/lib/utils/SessionManager'
|
||||
import { StateManager } from '@/lib/utils/StateManager'
|
||||
@@ -5,8 +8,6 @@ import { MemoryCache } from '@/lib/utils/cache/MemoryCache'
|
||||
import { hash } from '@/lib/utils/helper-functions'
|
||||
import { getToolsFromContext } from '@/tools'
|
||||
import type { Context } from '@/tools/types'
|
||||
import { type Tool as VercelTool, tool } from 'ai'
|
||||
import type { z } from 'zod'
|
||||
|
||||
/**
|
||||
* Options for the PostHog Agent Toolkit
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import { DynamicStructuredTool } from '@langchain/core/tools'
|
||||
import type { z } from 'zod'
|
||||
|
||||
import { ApiClient } from '@/api/client'
|
||||
import { SessionManager } from '@/lib/utils/SessionManager'
|
||||
import { StateManager } from '@/lib/utils/StateManager'
|
||||
@@ -5,8 +8,6 @@ import { MemoryCache } from '@/lib/utils/cache/MemoryCache'
|
||||
import { hash } from '@/lib/utils/helper-functions'
|
||||
import { getToolsFromContext } from '@/tools'
|
||||
import type { Context } from '@/tools/types'
|
||||
import { DynamicStructuredTool } from '@langchain/core/tools'
|
||||
import type { z } from 'zod'
|
||||
|
||||
/**
|
||||
* Options for the PostHog Agent Toolkit
|
||||
|
||||
@@ -51,26 +51,23 @@ export class MyMCP extends McpAgent<Env> {
|
||||
|
||||
_sessionManager: SessionManager | undefined
|
||||
|
||||
get requestProperties() {
|
||||
get requestProperties(): RequestProperties {
|
||||
return this.props as RequestProperties
|
||||
}
|
||||
|
||||
get cache() {
|
||||
get cache(): DurableObjectCache<State> {
|
||||
if (!this.requestProperties.userHash) {
|
||||
throw new Error('User hash is required to use the cache')
|
||||
}
|
||||
|
||||
if (!this._cache) {
|
||||
this._cache = new DurableObjectCache<State>(
|
||||
this.requestProperties.userHash,
|
||||
this.ctx.storage
|
||||
)
|
||||
this._cache = new DurableObjectCache<State>(this.requestProperties.userHash, this.ctx.storage)
|
||||
}
|
||||
|
||||
return this._cache
|
||||
}
|
||||
|
||||
get sessionManager() {
|
||||
get sessionManager(): SessionManager {
|
||||
if (!this._sessionManager) {
|
||||
this._sessionManager = new SessionManager(this.cache)
|
||||
}
|
||||
@@ -89,10 +86,7 @@ export class MyMCP extends McpAgent<Env> {
|
||||
baseUrl: 'https://eu.posthog.com',
|
||||
})
|
||||
|
||||
const [usResult, euResult] = await Promise.all([
|
||||
usClient.users().me(),
|
||||
euClient.users().me(),
|
||||
])
|
||||
const [usResult, euResult] = await Promise.all([usClient.users().me(), euClient.users().me()])
|
||||
|
||||
if (usResult.success) {
|
||||
await this.cache.set('region', 'us')
|
||||
@@ -107,7 +101,7 @@ export class MyMCP extends McpAgent<Env> {
|
||||
return undefined
|
||||
}
|
||||
|
||||
async getBaseUrl() {
|
||||
async getBaseUrl(): Promise<string> {
|
||||
if (CUSTOM_BASE_URL) {
|
||||
return CUSTOM_BASE_URL
|
||||
}
|
||||
@@ -121,7 +115,7 @@ export class MyMCP extends McpAgent<Env> {
|
||||
return 'https://us.posthog.com'
|
||||
}
|
||||
|
||||
async api() {
|
||||
async api(): Promise<ApiClient> {
|
||||
if (!this._api) {
|
||||
const baseUrl = await this.getBaseUrl()
|
||||
this._api = new ApiClient({
|
||||
@@ -133,7 +127,7 @@ export class MyMCP extends McpAgent<Env> {
|
||||
return this._api
|
||||
}
|
||||
|
||||
async getDistinctId() {
|
||||
async getDistinctId(): Promise<string> {
|
||||
let _distinctId = await this.cache.get('distinctId')
|
||||
|
||||
if (!_distinctId) {
|
||||
@@ -148,7 +142,7 @@ export class MyMCP extends McpAgent<Env> {
|
||||
return _distinctId
|
||||
}
|
||||
|
||||
async trackEvent(event: AnalyticsEvent, properties: Record<string, any> = {}) {
|
||||
async trackEvent(event: AnalyticsEvent, properties: Record<string, any> = {}): Promise<void> {
|
||||
try {
|
||||
const distinctId = await this.getDistinctId()
|
||||
|
||||
@@ -160,16 +154,14 @@ export class MyMCP extends McpAgent<Env> {
|
||||
properties: {
|
||||
...(this.requestProperties.sessionId
|
||||
? {
|
||||
$session_id: await this.sessionManager.getSessionUuid(
|
||||
this.requestProperties.sessionId
|
||||
),
|
||||
$session_id: await this.sessionManager.getSessionUuid(this.requestProperties.sessionId),
|
||||
}
|
||||
: {}),
|
||||
...properties,
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
//
|
||||
} catch {
|
||||
// skip
|
||||
}
|
||||
}
|
||||
|
||||
@@ -177,7 +169,7 @@ export class MyMCP extends McpAgent<Env> {
|
||||
tool: Tool<z.ZodObject<TSchema>>,
|
||||
handler: (params: z.infer<z.ZodObject<TSchema>>) => Promise<any>
|
||||
): void {
|
||||
const wrappedHandler = async (params: z.infer<z.ZodObject<TSchema>>) => {
|
||||
const wrappedHandler = async (params: z.infer<z.ZodObject<TSchema>>): Promise<any> => {
|
||||
const validation = tool.schema.safeParse(params)
|
||||
|
||||
if (!validation.success) {
|
||||
@@ -245,7 +237,7 @@ export class MyMCP extends McpAgent<Env> {
|
||||
}
|
||||
}
|
||||
|
||||
async init() {
|
||||
async init(): Promise<void> {
|
||||
const context = await this.getContext()
|
||||
|
||||
// Register prompts and resources
|
||||
|
||||
@@ -2,7 +2,7 @@ import { PostHog } from 'posthog-node'
|
||||
|
||||
let _client: PostHog | undefined
|
||||
|
||||
export const getPostHogClient = () => {
|
||||
export const getPostHogClient = (): PostHog => {
|
||||
if (!_client) {
|
||||
_client = new PostHog('sTMFPsFhdP1Ssg', {
|
||||
host: 'https://us.i.posthog.com',
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { getPostHogClient } from '@/integrations/mcp/utils/client'
|
||||
import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js'
|
||||
|
||||
import { getPostHogClient } from '@/integrations/mcp/utils/client'
|
||||
|
||||
export class MCPToolError extends Error {
|
||||
public readonly tool: string
|
||||
public readonly originalError: unknown
|
||||
@@ -13,22 +14,6 @@ export class MCPToolError extends Error {
|
||||
this.originalError = originalError
|
||||
this.timestamp = new Date()
|
||||
}
|
||||
|
||||
getTrackingData() {
|
||||
return {
|
||||
tool: this.tool,
|
||||
message: this.message,
|
||||
timestamp: this.timestamp.toISOString(),
|
||||
originalError:
|
||||
this.originalError instanceof Error
|
||||
? {
|
||||
name: this.originalError.name,
|
||||
message: this.originalError.message,
|
||||
stack: this.originalError.stack,
|
||||
}
|
||||
: String(this.originalError),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -45,20 +30,11 @@ export class MCPToolError extends Error {
|
||||
*
|
||||
* @returns A structured error message.
|
||||
*/
|
||||
export function handleToolError(
|
||||
error: any,
|
||||
tool?: string,
|
||||
distinctId?: string,
|
||||
sessionUuid?: string
|
||||
): CallToolResult {
|
||||
export function handleToolError(error: any, tool?: string, distinctId?: string, sessionUuid?: string): CallToolResult {
|
||||
const mcpError =
|
||||
error instanceof MCPToolError
|
||||
? error
|
||||
: new MCPToolError(
|
||||
error instanceof Error ? error.message : String(error),
|
||||
tool || 'unknown',
|
||||
error
|
||||
)
|
||||
: new MCPToolError(error instanceof Error ? error.message : String(error), tool || 'unknown', error)
|
||||
|
||||
const properties: Record<string, any> = {
|
||||
team: 'growth',
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { v7 as uuidv7 } from 'uuid'
|
||||
|
||||
import type { PrefixedString } from '@/lib/types'
|
||||
import type { ScopedCache } from '@/lib/utils/cache/ScopedCache'
|
||||
import type { State } from '@/tools'
|
||||
import { v7 as uuidv7 } from 'uuid'
|
||||
|
||||
export class SessionManager {
|
||||
private cache: ScopedCache<State>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { ApiClient } from '@/api/client'
|
||||
import type { ApiUser } from '@/schema/api'
|
||||
import type { State } from '@/tools/types'
|
||||
|
||||
import type { ScopedCache } from './cache/ScopedCache'
|
||||
|
||||
export class StateManager {
|
||||
@@ -13,7 +14,7 @@ export class StateManager {
|
||||
this._api = api
|
||||
}
|
||||
|
||||
private async _fetchUser() {
|
||||
private async _fetchUser(): Promise<ApiUser> {
|
||||
const userResult = await this._api.users().me()
|
||||
if (!userResult.success) {
|
||||
throw new Error(`Failed to get user: ${userResult.error.message}`)
|
||||
@@ -21,7 +22,7 @@ export class StateManager {
|
||||
return userResult.data
|
||||
}
|
||||
|
||||
async getUser() {
|
||||
async getUser(): Promise<ApiUser> {
|
||||
if (!this._user) {
|
||||
this._user = await this._fetchUser()
|
||||
}
|
||||
@@ -29,7 +30,7 @@ export class StateManager {
|
||||
return this._user
|
||||
}
|
||||
|
||||
private async _fetchApiKey() {
|
||||
private async _fetchApiKey(): Promise<NonNullable<State['apiKey']>> {
|
||||
const apiKeyResult = await this._api.apiKeys().current()
|
||||
if (!apiKeyResult.success) {
|
||||
throw new Error(`Failed to get API key: ${apiKeyResult.error.message}`)
|
||||
@@ -37,7 +38,7 @@ export class StateManager {
|
||||
return apiKeyResult.data
|
||||
}
|
||||
|
||||
async getApiKey() {
|
||||
async getApiKey(): Promise<NonNullable<State['apiKey']>> {
|
||||
let _apiKey = await this._cache.get('apiKey')
|
||||
|
||||
if (!_apiKey) {
|
||||
@@ -48,7 +49,7 @@ export class StateManager {
|
||||
return _apiKey
|
||||
}
|
||||
|
||||
async getDistinctId() {
|
||||
async getDistinctId(): Promise<NonNullable<State['distinctId']>> {
|
||||
let _distinctId = await this._cache.get('distinctId')
|
||||
|
||||
if (!_distinctId) {
|
||||
@@ -81,19 +82,13 @@ export class StateManager {
|
||||
return { projectId }
|
||||
}
|
||||
|
||||
if (
|
||||
scoped_organizations.length === 0 ||
|
||||
scoped_organizations.includes(activeOrganization.id)
|
||||
) {
|
||||
if (scoped_organizations.length === 0 || scoped_organizations.includes(activeOrganization.id)) {
|
||||
return { organizationId: activeOrganization.id, projectId: activeTeam.id }
|
||||
}
|
||||
|
||||
const organizationId = scoped_organizations[0]!
|
||||
|
||||
const projectsResult = await this._api
|
||||
.organizations()
|
||||
.projects({ orgId: organizationId })
|
||||
.list()
|
||||
const projectsResult = await this._api.organizations().projects({ orgId: organizationId }).list()
|
||||
|
||||
if (!projectsResult.success) {
|
||||
throw projectsResult.error
|
||||
@@ -108,7 +103,10 @@ export class StateManager {
|
||||
return { organizationId, projectId: Number(projectId) }
|
||||
}
|
||||
|
||||
async setDefaultOrganizationAndProject() {
|
||||
async setDefaultOrganizationAndProject(): Promise<{
|
||||
organizationId: string | undefined
|
||||
projectId: number
|
||||
}> {
|
||||
const { organizationId, projectId } = await this._getDefaultOrganizationAndProject()
|
||||
|
||||
if (organizationId) {
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
import { ApiListResponseSchema } from '@/schema/api'
|
||||
|
||||
import type { z } from 'zod'
|
||||
|
||||
export const withPagination = async <T>(
|
||||
url: string,
|
||||
apiToken: string,
|
||||
dataSchema: z.ZodType<T>
|
||||
): Promise<T[]> => {
|
||||
import { ApiListResponseSchema } from '@/schema/api'
|
||||
|
||||
export const withPagination = async <T>(url: string, apiToken: string, dataSchema: z.ZodType<T>): Promise<T[]> => {
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiToken}`,
|
||||
@@ -33,22 +29,19 @@ export const withPagination = async <T>(
|
||||
return results
|
||||
}
|
||||
|
||||
export const hasScope = (scopes: string[], requiredScope: string) => {
|
||||
export const hasScope = (scopes: string[], requiredScope: string): boolean => {
|
||||
if (scopes.includes('*')) {
|
||||
return true
|
||||
}
|
||||
|
||||
// if read scoped required, and write present, return true
|
||||
if (
|
||||
requiredScope.endsWith(':read') &&
|
||||
scopes.includes(requiredScope.replace(':read', ':write'))
|
||||
) {
|
||||
if (requiredScope.endsWith(':read') && scopes.includes(requiredScope.replace(':read', ':write'))) {
|
||||
return true
|
||||
}
|
||||
|
||||
return scopes.includes(requiredScope)
|
||||
}
|
||||
|
||||
export const hasScopes = (scopes: string[], requiredScopes: string[]) => {
|
||||
export const hasScopes = (scopes: string[], requiredScopes: string[]): boolean => {
|
||||
return requiredScopes.every((scope) => hasScope(scopes, scope))
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import crypto from 'node:crypto'
|
||||
|
||||
export function hash(data: string) {
|
||||
export function hash(data: string): string {
|
||||
// Use PBKDF2 with sufficient computational effort for security
|
||||
// 100,000 iterations provides good security while maintaining reasonable performance
|
||||
const salt = crypto.createHash('sha256').update('posthog_mcp_salt').digest()
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
|
||||
import type { z } from 'zod'
|
||||
|
||||
import type { Context } from '@/tools/types'
|
||||
|
||||
import { setupEventsPrompt } from './setup-events'
|
||||
|
||||
export interface Prompt {
|
||||
@@ -27,7 +28,7 @@ export async function getPromptsFromContext(context: Context): Promise<Prompt[]>
|
||||
return [await setupEventsPrompt(context)]
|
||||
}
|
||||
|
||||
export async function registerPrompts(server: McpServer, context: Context) {
|
||||
export async function registerPrompts(server: McpServer, context: Context): Promise<void> {
|
||||
const prompts = await getPromptsFromContext(context)
|
||||
|
||||
for (const prompt of prompts) {
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
import type { Context } from '@/tools/types'
|
||||
import type { Prompt } from './index'
|
||||
import { ResourceUri } from '@/resources'
|
||||
import type { Context } from '@/tools/types'
|
||||
|
||||
import type { Prompt } from './index'
|
||||
|
||||
// oxlint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
export async function setupEventsPrompt(_context: Context): Promise<Prompt> {
|
||||
return {
|
||||
name: 'posthog-setup',
|
||||
title: 'Setup a deep PostHog integration',
|
||||
description:
|
||||
'Automatically instrument your Next.js project with PostHog event tracking, user identification, and error tracking',
|
||||
handler: async (_context, _args) => {
|
||||
handler: async () => {
|
||||
return {
|
||||
messages: [
|
||||
{
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
|
||||
|
||||
import type { Context } from '@/tools/types'
|
||||
|
||||
import { registerIntegrationResources } from './integration'
|
||||
|
||||
// Re-export ResourceUri for external consumers
|
||||
export { ResourceUri } from './integration'
|
||||
|
||||
export function registerResources(server: McpServer, context: Context) {
|
||||
export function registerResources(server: McpServer, context: Context): void {
|
||||
registerIntegrationResources(server, context)
|
||||
}
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import { type McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js'
|
||||
import { type Unzipped, strFromU8, unzipSync } from 'fflate'
|
||||
|
||||
import type { Context } from '@/tools/types'
|
||||
|
||||
import {
|
||||
FRAMEWORK_DOCS,
|
||||
EXAMPLES_MARKDOWN_URL,
|
||||
FRAMEWORK_DOCS,
|
||||
FRAMEWORK_MARKDOWN_FILES,
|
||||
isSupportedFramework,
|
||||
getSupportedFrameworks,
|
||||
getSupportedFrameworksList,
|
||||
isSupportedFramework,
|
||||
} from './framework-mappings'
|
||||
import { unzipSync, strFromU8, type Unzipped } from 'fflate'
|
||||
|
||||
// Import workflow markdown files
|
||||
import workflowBegin from './workflow-guides/1.0-event-setup-begin.md'
|
||||
import workflowEdit from './workflow-guides/1.1-event-setup-edit.md'
|
||||
@@ -35,8 +36,7 @@ export enum ResourceUri {
|
||||
/**
|
||||
* Message appended to workflow resources to indicate the next step
|
||||
*/
|
||||
export const WORKFLOW_NEXT_STEP_MESSAGE =
|
||||
'Upon completion, access the following resource to continue:'
|
||||
export const WORKFLOW_NEXT_STEP_MESSAGE = 'Upon completion, access the following resource to continue:'
|
||||
|
||||
/**
|
||||
* URL to the identify() documentation
|
||||
@@ -67,7 +67,6 @@ async function fetchExamplesMarkdown(): Promise<Unzipped> {
|
||||
return cachedExamplesMarkdown
|
||||
}
|
||||
|
||||
console.log('Fetching PostHog examples markdown...')
|
||||
const response = await fetch(EXAMPLES_MARKDOWN_URL)
|
||||
|
||||
if (!response.ok) {
|
||||
@@ -77,7 +76,6 @@ async function fetchExamplesMarkdown(): Promise<Unzipped> {
|
||||
const arrayBuffer = await response.arrayBuffer()
|
||||
const uint8Array = new Uint8Array(arrayBuffer)
|
||||
cachedExamplesMarkdown = unzipSync(uint8Array)
|
||||
console.log('Examples markdown cached successfully')
|
||||
|
||||
return cachedExamplesMarkdown
|
||||
}
|
||||
@@ -107,7 +105,8 @@ const workflowSequence = [
|
||||
/**
|
||||
* Registers all PostHog integration resources with the MCP server
|
||||
*/
|
||||
export function registerIntegrationResources(server: McpServer, _context: Context) {
|
||||
// oxlint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
export function registerIntegrationResources(server: McpServer, _context: Context): void {
|
||||
// Fetch examples markdown at startup
|
||||
fetchExamplesMarkdown().catch((error) => {
|
||||
console.error('Failed to fetch examples markdown:', error)
|
||||
@@ -216,10 +215,7 @@ export function registerIntegrationResources(server: McpServer, _context: Contex
|
||||
const frameworks = getSupportedFrameworks()
|
||||
return {
|
||||
resources: frameworks.map((framework) => ({
|
||||
uri: ResourceUri.EXAMPLE_PROJECT_FRAMEWORK.replace(
|
||||
'{framework}',
|
||||
framework
|
||||
),
|
||||
uri: ResourceUri.EXAMPLE_PROJECT_FRAMEWORK.replace('{framework}', framework),
|
||||
name: `PostHog ${framework} example project`,
|
||||
description: `Example project code for ${framework}`,
|
||||
mimeType: 'text/markdown',
|
||||
|
||||
@@ -35,6 +35,7 @@ export const ApiEventDefinitionSchema = z.object({
|
||||
tags: z.array(z.string().nullish()).nullish(),
|
||||
})
|
||||
|
||||
// oxlint-disable-next-line @typescript-eslint/explicit-function-return-type
|
||||
export const ApiListResponseSchema = <T extends z.ZodType>(dataSchema: T) =>
|
||||
z.object({
|
||||
count: z.number().nullish(),
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { z } from 'zod'
|
||||
|
||||
import { FeatureFlagSchema } from './flags'
|
||||
import {
|
||||
ExperimentCreateSchema as ToolExperimentCreateSchema,
|
||||
@@ -237,7 +238,7 @@ export type ExperimentUpdateApiPayload = z.infer<typeof ExperimentUpdateApiPaylo
|
||||
/**
|
||||
* Helper to conditionally add properties only if they exist and are not empty
|
||||
*/
|
||||
const getPropertiesIfNotEmpty = (props: any) => {
|
||||
const getPropertiesIfNotEmpty = <T>(props: T): { properties?: T } => {
|
||||
return props && Object.keys(props).length > 0 ? { properties: props } : {}
|
||||
}
|
||||
|
||||
@@ -365,67 +366,61 @@ export type ExperimentCreatePayload = z.output<typeof ExperimentCreatePayloadSch
|
||||
* Transform user-friendly update input to API payload format for experiment updates
|
||||
* This handles partial updates with the same transformation patterns as creation
|
||||
*/
|
||||
export const ExperimentUpdateTransformSchema = ToolExperimentUpdateInputSchema.transform(
|
||||
(input) => {
|
||||
const updatePayload: Record<string, any> = {}
|
||||
export const ExperimentUpdateTransformSchema = ToolExperimentUpdateInputSchema.transform((input) => {
|
||||
const updatePayload: Record<string, any> = {}
|
||||
|
||||
// Basic fields - direct mapping
|
||||
if (input.name !== undefined) {
|
||||
updatePayload.name = input.name
|
||||
}
|
||||
if (input.description !== undefined) {
|
||||
updatePayload.description = input.description
|
||||
}
|
||||
|
||||
// Transform metrics if provided
|
||||
if (input.primary_metrics !== undefined) {
|
||||
updatePayload.metrics = input.primary_metrics.map(transformMetricToApi)
|
||||
updatePayload.primary_metrics_ordered_uuids = updatePayload.metrics.map(
|
||||
(m: any) => m.uuid!
|
||||
)
|
||||
}
|
||||
|
||||
if (input.secondary_metrics !== undefined) {
|
||||
updatePayload.metrics_secondary = input.secondary_metrics.map(transformMetricToApi)
|
||||
updatePayload.secondary_metrics_ordered_uuids = updatePayload.metrics_secondary.map(
|
||||
(m: any) => m.uuid!
|
||||
)
|
||||
}
|
||||
|
||||
// Transform minimum detectable effect into parameters
|
||||
if (input.minimum_detectable_effect !== undefined) {
|
||||
updatePayload.parameters = {
|
||||
...updatePayload.parameters,
|
||||
minimum_detectable_effect: input.minimum_detectable_effect,
|
||||
}
|
||||
}
|
||||
|
||||
// Handle experiment state management
|
||||
if (input.launch === true) {
|
||||
updatePayload.start_date = new Date().toISOString()
|
||||
}
|
||||
|
||||
if (input.conclude !== undefined) {
|
||||
updatePayload.conclusion = input.conclude
|
||||
updatePayload.end_date = new Date().toISOString()
|
||||
if (input.conclusion_comment !== undefined) {
|
||||
updatePayload.conclusion_comment = input.conclusion_comment
|
||||
}
|
||||
}
|
||||
|
||||
if (input.restart === true) {
|
||||
updatePayload.end_date = null
|
||||
updatePayload.conclusion = null
|
||||
updatePayload.conclusion_comment = null
|
||||
}
|
||||
|
||||
if (input.archive !== undefined) {
|
||||
updatePayload.archived = input.archive
|
||||
}
|
||||
|
||||
return updatePayload
|
||||
// Basic fields - direct mapping
|
||||
if (input.name !== undefined) {
|
||||
updatePayload.name = input.name
|
||||
}
|
||||
).pipe(ExperimentUpdateApiPayloadSchema)
|
||||
if (input.description !== undefined) {
|
||||
updatePayload.description = input.description
|
||||
}
|
||||
|
||||
// Transform metrics if provided
|
||||
if (input.primary_metrics !== undefined) {
|
||||
updatePayload.metrics = input.primary_metrics.map(transformMetricToApi)
|
||||
updatePayload.primary_metrics_ordered_uuids = updatePayload.metrics.map((m: any) => m.uuid!)
|
||||
}
|
||||
|
||||
if (input.secondary_metrics !== undefined) {
|
||||
updatePayload.metrics_secondary = input.secondary_metrics.map(transformMetricToApi)
|
||||
updatePayload.secondary_metrics_ordered_uuids = updatePayload.metrics_secondary.map((m: any) => m.uuid!)
|
||||
}
|
||||
|
||||
// Transform minimum detectable effect into parameters
|
||||
if (input.minimum_detectable_effect !== undefined) {
|
||||
updatePayload.parameters = {
|
||||
...updatePayload.parameters,
|
||||
minimum_detectable_effect: input.minimum_detectable_effect,
|
||||
}
|
||||
}
|
||||
|
||||
// Handle experiment state management
|
||||
if (input.launch === true) {
|
||||
updatePayload.start_date = new Date().toISOString()
|
||||
}
|
||||
|
||||
if (input.conclude !== undefined) {
|
||||
updatePayload.conclusion = input.conclude
|
||||
updatePayload.end_date = new Date().toISOString()
|
||||
if (input.conclusion_comment !== undefined) {
|
||||
updatePayload.conclusion_comment = input.conclusion_comment
|
||||
}
|
||||
}
|
||||
|
||||
if (input.restart === true) {
|
||||
updatePayload.end_date = null
|
||||
updatePayload.conclusion = null
|
||||
updatePayload.conclusion_comment = null
|
||||
}
|
||||
|
||||
if (input.archive !== undefined) {
|
||||
updatePayload.archived = input.archive
|
||||
}
|
||||
|
||||
return updatePayload
|
||||
}).pipe(ExperimentUpdateApiPayloadSchema)
|
||||
|
||||
export type ExperimentUpdateTransform = z.output<typeof ExperimentUpdateTransformSchema>
|
||||
|
||||
|
||||
@@ -10,36 +10,21 @@ export interface PostHogFlagsResponse {
|
||||
results?: PostHogFeatureFlag[]
|
||||
}
|
||||
const base = ['exact', 'is_not', 'is_set', 'is_not_set'] as const
|
||||
const stringOps = [
|
||||
...base,
|
||||
'icontains',
|
||||
'not_icontains',
|
||||
'regex',
|
||||
'not_regex',
|
||||
'is_cleaned_path_exact',
|
||||
] as const
|
||||
const stringOps = [...base, 'icontains', 'not_icontains', 'regex', 'not_regex', 'is_cleaned_path_exact'] as const
|
||||
const numberOps = [...base, 'gt', 'gte', 'lt', 'lte', 'min', 'max'] as const
|
||||
const booleanOps = [...base] as const
|
||||
|
||||
const arrayOps = ['in', 'not_in'] as const
|
||||
|
||||
const operatorSchema = z.enum([
|
||||
...stringOps,
|
||||
...numberOps,
|
||||
...booleanOps,
|
||||
...arrayOps,
|
||||
] as unknown as [string, ...string[]])
|
||||
const operatorSchema = z.enum([...stringOps, ...numberOps, ...booleanOps, ...arrayOps] as unknown as [
|
||||
string,
|
||||
...string[],
|
||||
])
|
||||
|
||||
export const PersonPropertyFilterSchema = z
|
||||
.object({
|
||||
key: z.string(),
|
||||
value: z.union([
|
||||
z.string(),
|
||||
z.number(),
|
||||
z.boolean(),
|
||||
z.array(z.string()),
|
||||
z.array(z.number()),
|
||||
]),
|
||||
value: z.union([z.string(), z.number(), z.boolean(), z.array(z.string()), z.array(z.number())]),
|
||||
operator: operatorSchema.optional(),
|
||||
})
|
||||
.superRefine((data, ctx) => {
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
import {
|
||||
ApiEventDefinitionSchema,
|
||||
type ApiListResponseSchema,
|
||||
ApiPropertyDefinitionSchema,
|
||||
} from '@/schema/api'
|
||||
import type { z } from 'zod'
|
||||
|
||||
import { ApiEventDefinitionSchema, type ApiListResponseSchema, ApiPropertyDefinitionSchema } from '@/schema/api'
|
||||
|
||||
export const PropertyDefinitionSchema = ApiPropertyDefinitionSchema.pick({
|
||||
name: true,
|
||||
property_type: true,
|
||||
|
||||
@@ -198,10 +198,7 @@ const DataVisualizationNodeSchema = z.object({
|
||||
})
|
||||
|
||||
// Any insight query
|
||||
const InsightQuerySchema = z.discriminatedUnion('kind', [
|
||||
InsightVizNodeSchema,
|
||||
DataVisualizationNodeSchema,
|
||||
])
|
||||
const InsightQuerySchema = z.discriminatedUnion('kind', [InsightVizNodeSchema, DataVisualizationNodeSchema])
|
||||
|
||||
// Export all schemas
|
||||
export {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { z } from 'zod'
|
||||
|
||||
import { FilterGroupsSchema } from './flags.js'
|
||||
|
||||
// Survey question types
|
||||
@@ -29,9 +30,7 @@ const ChoiceResponseBranching = z
|
||||
"Only include keys for responses that should branch to a specific question or 'end'. Omit keys for responses that should proceed to the next question (default behavior)."
|
||||
),
|
||||
})
|
||||
.describe(
|
||||
'For single choice questions: use choice indices as string keys ("0", "1", "2", etc.)'
|
||||
)
|
||||
.describe('For single choice questions: use choice indices as string keys ("0", "1", "2", etc.)')
|
||||
|
||||
// NPS sentiment branching - uses sentiment categories
|
||||
const NPSSentimentBranching = z
|
||||
@@ -41,9 +40,7 @@ const NPSSentimentBranching = z
|
||||
.record(
|
||||
z
|
||||
.enum(['detractors', 'passives', 'promoters'])
|
||||
.describe(
|
||||
'NPS sentiment categories: detractors (0-6), passives (7-8), promoters (9-10)'
|
||||
),
|
||||
.describe('NPS sentiment categories: detractors (0-6), passives (7-8), promoters (9-10)'),
|
||||
z.union([z.number(), z.literal('end')])
|
||||
)
|
||||
.describe(
|
||||
@@ -95,12 +92,7 @@ const ChoiceBranching = z.union([
|
||||
SpecificQuestionBranching,
|
||||
])
|
||||
|
||||
const NPSBranching = z.union([
|
||||
NextQuestionBranching,
|
||||
EndBranching,
|
||||
NPSSentimentBranching,
|
||||
SpecificQuestionBranching,
|
||||
])
|
||||
const NPSBranching = z.union([NextQuestionBranching, EndBranching, NPSSentimentBranching, SpecificQuestionBranching])
|
||||
|
||||
const RatingBranching = z.union([
|
||||
NextQuestionBranching,
|
||||
@@ -129,14 +121,8 @@ const RatingQuestion = BaseSurveyQuestionSchema.extend({
|
||||
.union([z.literal(3), z.literal(5), z.literal(7)])
|
||||
.optional()
|
||||
.describe('Rating scale can be one of 3, 5, or 7'),
|
||||
lowerBoundLabel: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("Label for the lowest rating (e.g., 'Very Poor')"),
|
||||
upperBoundLabel: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("Label for the highest rating (e.g., 'Excellent')"),
|
||||
lowerBoundLabel: z.string().optional().describe("Label for the lowest rating (e.g., 'Very Poor')"),
|
||||
upperBoundLabel: z.string().optional().describe("Label for the highest rating (e.g., 'Excellent')"),
|
||||
branching: RatingBranching.optional(),
|
||||
}).superRefine((data, ctx) => {
|
||||
// Validate display-specific scale constraints
|
||||
@@ -178,14 +164,8 @@ const NPSRatingQuestion = BaseSurveyQuestionSchema.extend({
|
||||
type: z.literal('rating'),
|
||||
display: z.literal('number').describe('NPS questions always use numeric scale'),
|
||||
scale: z.literal(10).describe('NPS questions always use 0-10 scale'),
|
||||
lowerBoundLabel: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("Label for 0 rating (typically 'Not at all likely')"),
|
||||
upperBoundLabel: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("Label for 10 rating (typically 'Extremely likely')"),
|
||||
lowerBoundLabel: z.string().optional().describe("Label for 0 rating (typically 'Not at all likely')"),
|
||||
upperBoundLabel: z.string().optional().describe("Label for 10 rating (typically 'Extremely likely')"),
|
||||
branching: NPSBranching.optional(),
|
||||
}).superRefine((data, ctx) => {
|
||||
// Validate response-based branching for NPS rating questions
|
||||
@@ -212,13 +192,8 @@ const SingleChoiceQuestion = BaseSurveyQuestionSchema.extend({
|
||||
.array(z.string().min(1, 'Choice text cannot be empty'))
|
||||
.min(2, 'Must have at least 2 choices')
|
||||
.max(20, 'Cannot have more than 20 choices')
|
||||
.describe(
|
||||
'Array of choice options. Choice indices (0, 1, 2, etc.) are used for branching logic'
|
||||
),
|
||||
shuffleOptions: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.describe('Whether to randomize the order of choices for each respondent'),
|
||||
.describe('Array of choice options. Choice indices (0, 1, 2, etc.) are used for branching logic'),
|
||||
shuffleOptions: z.boolean().optional().describe('Whether to randomize the order of choices for each respondent'),
|
||||
hasOpenChoice: z
|
||||
.boolean()
|
||||
.optional()
|
||||
@@ -269,13 +244,8 @@ const MultipleChoiceQuestion = BaseSurveyQuestionSchema.extend({
|
||||
.array(z.string().min(1, 'Choice text cannot be empty'))
|
||||
.min(2, 'Must have at least 2 choices')
|
||||
.max(20, 'Cannot have more than 20 choices')
|
||||
.describe(
|
||||
'Array of choice options. Multiple selections allowed. No branching logic supported.'
|
||||
),
|
||||
shuffleOptions: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.describe('Whether to randomize the order of choices for each respondent'),
|
||||
.describe('Array of choice options. Multiple selections allowed. No branching logic supported.'),
|
||||
shuffleOptions: z.boolean().optional().describe('Whether to randomize the order of choices for each respondent'),
|
||||
hasOpenChoice: z
|
||||
.boolean()
|
||||
.optional()
|
||||
@@ -360,10 +330,7 @@ const SurveyConditions = z.object({
|
||||
.optional(),
|
||||
deviceTypes: z.array(z.enum(['Desktop', 'Mobile', 'Tablet'])).optional(),
|
||||
deviceTypesMatchType: MatchTypeEnum.optional(),
|
||||
linkedFlagVariant: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe('The variant of the feature flag linked to this survey'),
|
||||
linkedFlagVariant: z.string().optional().describe('The variant of the feature flag linked to this survey'),
|
||||
})
|
||||
|
||||
// Survey appearance customization - input schema
|
||||
@@ -453,11 +420,7 @@ export const CreateSurveyInputSchema = z.object({
|
||||
.describe(
|
||||
'When at least one question is answered, the response is stored (true). The response is stored when all questions are answered (false).'
|
||||
),
|
||||
linked_flag_id: z
|
||||
.number()
|
||||
.nullable()
|
||||
.optional()
|
||||
.describe('The feature flag linked to this survey'),
|
||||
linked_flag_id: z.number().nullable().optional().describe('The feature flag linked to this survey'),
|
||||
targeting_flag_filters: FilterGroupsSchema.optional().describe(
|
||||
"Target specific users based on their properties. Example: {groups: [{properties: [{key: 'email', value: ['@company.com'], operator: 'icontains'}], rollout_percentage: 100}]}"
|
||||
),
|
||||
@@ -467,10 +430,7 @@ export const UpdateSurveyInputSchema = z.object({
|
||||
name: z.string().min(1, 'Survey name cannot be empty').optional(),
|
||||
description: z.string().optional(),
|
||||
type: z.enum(['popover', 'api', 'widget', 'external_survey']).optional(),
|
||||
questions: z
|
||||
.array(SurveyQuestionInputSchema)
|
||||
.min(1, 'Survey must have at least one question')
|
||||
.optional(),
|
||||
questions: z.array(SurveyQuestionInputSchema).min(1, 'Survey must have at least one question').optional(),
|
||||
conditions: SurveyConditions.optional(),
|
||||
appearance: SurveyAppearance.optional(),
|
||||
schedule: z
|
||||
@@ -483,16 +443,12 @@ export const UpdateSurveyInputSchema = z.object({
|
||||
.string()
|
||||
.datetime()
|
||||
.optional()
|
||||
.describe(
|
||||
'When the survey should start being shown to users. Setting this will launch the survey'
|
||||
),
|
||||
.describe('When the survey should start being shown to users. Setting this will launch the survey'),
|
||||
end_date: z
|
||||
.string()
|
||||
.datetime()
|
||||
.optional()
|
||||
.describe(
|
||||
'When the survey stopped being shown to users. Setting this will complete the survey.'
|
||||
),
|
||||
.describe('When the survey stopped being shown to users. Setting this will complete the survey.'),
|
||||
archived: z.boolean().optional(),
|
||||
responses_limit: z
|
||||
.number()
|
||||
@@ -523,15 +479,8 @@ export const UpdateSurveyInputSchema = z.object({
|
||||
.describe(
|
||||
'When at least one question is answered, the response is stored (true). The response is stored when all questions are answered (false).'
|
||||
),
|
||||
linked_flag_id: z
|
||||
.number()
|
||||
.nullable()
|
||||
.optional()
|
||||
.describe('The feature flag to link to this survey'),
|
||||
targeting_flag_id: z
|
||||
.number()
|
||||
.optional()
|
||||
.describe('An existing targeting flag to use for this survey'),
|
||||
linked_flag_id: z.number().nullable().optional().describe('The feature flag to link to this survey'),
|
||||
targeting_flag_id: z.number().optional().describe('An existing targeting flag to use for this survey'),
|
||||
targeting_flag_filters: FilterGroupsSchema.optional().describe(
|
||||
"Target specific users based on their properties. Example: {groups: [{properties: [{key: 'email', value: ['@company.com'], operator: 'icontains'}], rollout_percentage: 50}]}"
|
||||
),
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { z } from 'zod'
|
||||
|
||||
import {
|
||||
AddInsightToDashboardSchema,
|
||||
CreateDashboardInputSchema,
|
||||
@@ -84,9 +85,7 @@ export const ExperimentUpdateInputSchema = z.object({
|
||||
.describe(
|
||||
"Metric type: 'mean' for average values, 'funnel' for conversion flows, 'ratio' for comparing two metrics"
|
||||
),
|
||||
event_name: z
|
||||
.string()
|
||||
.describe("PostHog event name (e.g., '$pageview', 'add_to_cart', 'purchase')"),
|
||||
event_name: z.string().describe("PostHog event name (e.g., '$pageview', 'add_to_cart', 'purchase')"),
|
||||
funnel_steps: z
|
||||
.array(z.string())
|
||||
.optional()
|
||||
@@ -104,10 +103,7 @@ export const ExperimentUpdateInputSchema = z.object({
|
||||
name: z.string().optional().describe('Human-readable metric name'),
|
||||
metric_type: z.enum(['mean', 'funnel', 'ratio']).describe('Metric type'),
|
||||
event_name: z.string().describe('PostHog event name'),
|
||||
funnel_steps: z
|
||||
.array(z.string())
|
||||
.optional()
|
||||
.describe('For funnel metrics only: Array of event names'),
|
||||
funnel_steps: z.array(z.string()).optional().describe('For funnel metrics only: Array of event names'),
|
||||
properties: z.record(z.any()).optional().describe('Event properties to filter on'),
|
||||
description: z.string().optional().describe('What this metric measures'),
|
||||
})
|
||||
@@ -115,10 +111,7 @@ export const ExperimentUpdateInputSchema = z.object({
|
||||
.optional()
|
||||
.describe('Update secondary metrics'),
|
||||
|
||||
minimum_detectable_effect: z
|
||||
.number()
|
||||
.optional()
|
||||
.describe('Update minimum detectable effect in percentage'),
|
||||
minimum_detectable_effect: z.number().optional().describe('Update minimum detectable effect in percentage'),
|
||||
|
||||
// Experiment state management
|
||||
launch: z.boolean().optional().describe('Launch experiment (set start_date) or keep as draft'),
|
||||
@@ -130,26 +123,18 @@ export const ExperimentUpdateInputSchema = z.object({
|
||||
|
||||
conclusion_comment: z.string().optional().describe('Comment about experiment conclusion'),
|
||||
|
||||
restart: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.describe('Restart concluded experiment (clears end_date and conclusion)'),
|
||||
restart: z.boolean().optional().describe('Restart concluded experiment (clears end_date and conclusion)'),
|
||||
|
||||
archive: z.boolean().optional().describe('Archive or unarchive experiment'),
|
||||
})
|
||||
|
||||
export const ExperimentUpdateSchema = z.object({
|
||||
experimentId: z.number().describe('The ID of the experiment to update'),
|
||||
data: ExperimentUpdateInputSchema.describe(
|
||||
'The experiment data to update using user-friendly format'
|
||||
),
|
||||
data: ExperimentUpdateInputSchema.describe('The experiment data to update using user-friendly format'),
|
||||
})
|
||||
|
||||
export const ExperimentCreateSchema = z.object({
|
||||
name: z
|
||||
.string()
|
||||
.min(1)
|
||||
.describe('Experiment name - should clearly describe what is being tested'),
|
||||
name: z.string().min(1).describe('Experiment name - should clearly describe what is being tested'),
|
||||
|
||||
description: z
|
||||
.string()
|
||||
@@ -167,9 +152,7 @@ export const ExperimentCreateSchema = z.object({
|
||||
type: z
|
||||
.enum(['product', 'web'])
|
||||
.default('product')
|
||||
.describe(
|
||||
"Experiment type: 'product' for backend/API changes, 'web' for frontend UI changes"
|
||||
),
|
||||
.describe("Experiment type: 'product' for backend/API changes, 'web' for frontend UI changes"),
|
||||
|
||||
// Primary metrics with guidance
|
||||
primary_metrics: z
|
||||
@@ -196,9 +179,7 @@ export const ExperimentCreateSchema = z.object({
|
||||
description: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe(
|
||||
"What this metric measures and why it's important for the experiment"
|
||||
),
|
||||
.describe("What this metric measures and why it's important for the experiment"),
|
||||
})
|
||||
)
|
||||
.optional()
|
||||
@@ -216,9 +197,7 @@ export const ExperimentCreateSchema = z.object({
|
||||
.describe(
|
||||
"Metric type: 'mean' for average values, 'funnel' for conversion flows, 'ratio' for comparing two metrics"
|
||||
),
|
||||
event_name: z
|
||||
.string()
|
||||
.describe("REQUIRED: PostHog event name. Use '$pageview' if unsure."),
|
||||
event_name: z.string().describe("REQUIRED: PostHog event name. Use '$pageview' if unsure."),
|
||||
funnel_steps: z
|
||||
.array(z.string())
|
||||
.optional()
|
||||
@@ -236,15 +215,9 @@ export const ExperimentCreateSchema = z.object({
|
||||
variants: z
|
||||
.array(
|
||||
z.object({
|
||||
key: z
|
||||
.string()
|
||||
.describe("Variant key (e.g., 'control', 'variant_a', 'new_design')"),
|
||||
key: z.string().describe("Variant key (e.g., 'control', 'variant_a', 'new_design')"),
|
||||
name: z.string().optional().describe('Human-readable variant name'),
|
||||
rollout_percentage: z
|
||||
.number()
|
||||
.min(0)
|
||||
.max(100)
|
||||
.describe('Percentage of users to show this variant'),
|
||||
rollout_percentage: z.number().min(0).max(100).describe('Percentage of users to show this variant'),
|
||||
})
|
||||
)
|
||||
.optional()
|
||||
@@ -261,10 +234,7 @@ export const ExperimentCreateSchema = z.object({
|
||||
),
|
||||
|
||||
// Exposure and targeting
|
||||
filter_test_accounts: z
|
||||
.boolean()
|
||||
.default(true)
|
||||
.describe('Whether to filter out internal test accounts'),
|
||||
filter_test_accounts: z.boolean().default(true).describe('Whether to filter out internal test accounts'),
|
||||
|
||||
target_properties: z
|
||||
.record(z.any())
|
||||
@@ -275,16 +245,12 @@ export const ExperimentCreateSchema = z.object({
|
||||
draft: z
|
||||
.boolean()
|
||||
.default(true)
|
||||
.describe(
|
||||
'Create as draft (true) or launch immediately (false). Recommend draft for review first'
|
||||
),
|
||||
.describe('Create as draft (true) or launch immediately (false). Recommend draft for review first'),
|
||||
|
||||
holdout_id: z
|
||||
.number()
|
||||
.optional()
|
||||
.describe(
|
||||
'Holdout group ID if this experiment should exclude users from other experiments'
|
||||
),
|
||||
.describe('Holdout group ID if this experiment should exclude users from other experiments'),
|
||||
})
|
||||
|
||||
export const FeatureFlagCreateSchema = z.object({
|
||||
@@ -360,22 +326,13 @@ export const OrganizationSetActiveSchema = z.object({
|
||||
export const ProjectGetAllSchema = z.object({})
|
||||
|
||||
export const ProjectEventDefinitionsSchema = z.object({
|
||||
q: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe('Search query to filter event names. Only use if there are lots of events.'),
|
||||
q: z.string().optional().describe('Search query to filter event names. Only use if there are lots of events.'),
|
||||
})
|
||||
|
||||
export const ProjectPropertyDefinitionsInputSchema = z.object({
|
||||
type: z.enum(['event', 'person']).describe('Type of properties to get'),
|
||||
eventName: z
|
||||
.string()
|
||||
.describe('Event name to filter properties by, required for event type')
|
||||
.optional(),
|
||||
includePredefinedProperties: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.describe('Whether to include predefined properties'),
|
||||
eventName: z.string().describe('Event name to filter properties by, required for event type').optional(),
|
||||
includePredefinedProperties: z.boolean().optional().describe('Whether to include predefined properties'),
|
||||
})
|
||||
|
||||
export const ProjectSetActiveSchema = z.object({
|
||||
|
||||
@@ -22,12 +22,12 @@ Define your tool's input schema using Zod. Keep inputs **simple and user-friendl
|
||||
|
||||
```typescript
|
||||
export const FeatureFlagCreateSchema = z.object({
|
||||
name: z.string(),
|
||||
key: z.string(),
|
||||
description: z.string(),
|
||||
filters: FilterGroupsSchema,
|
||||
active: z.boolean(),
|
||||
tags: z.array(z.string()).optional(),
|
||||
name: z.string(),
|
||||
key: z.string(),
|
||||
description: z.string(),
|
||||
filters: FilterGroupsSchema,
|
||||
active: z.boolean(),
|
||||
tags: z.array(z.string()).optional(),
|
||||
})
|
||||
```
|
||||
|
||||
@@ -41,41 +41,42 @@ export const FeatureFlagCreateSchema = z.object({
|
||||
### 2. Create Tool Handler (`tools/featureFlags/create.ts`)
|
||||
|
||||
```typescript
|
||||
import type { z } from 'zod'
|
||||
|
||||
import { FeatureFlagCreateSchema } from '@/schema/tool-inputs'
|
||||
import type { Context, ToolBase } from '@/tools/types'
|
||||
import type { z } from 'zod'
|
||||
|
||||
const schema = FeatureFlagCreateSchema
|
||||
type Params = z.infer<typeof schema>
|
||||
|
||||
export const createHandler = async (context: Context, params: Params) => {
|
||||
const { name, key, description, filters, active, tags } = params
|
||||
const projectId = await context.stateManager.getProjectId()
|
||||
const { name, key, description, filters, active, tags } = params
|
||||
const projectId = await context.stateManager.getProjectId()
|
||||
|
||||
// Call API client method
|
||||
const flagResult = await context.api.featureFlags({ projectId }).create({
|
||||
data: { name, key, description, filters, active, tags },
|
||||
})
|
||||
// Call API client method
|
||||
const flagResult = await context.api.featureFlags({ projectId }).create({
|
||||
data: { name, key, description, filters, active, tags },
|
||||
})
|
||||
|
||||
if (!flagResult.success) {
|
||||
throw new Error(`Failed to create feature flag: ${flagResult.error.message}`)
|
||||
}
|
||||
if (!flagResult.success) {
|
||||
throw new Error(`Failed to create feature flag: ${flagResult.error.message}`)
|
||||
}
|
||||
|
||||
// Add context that is useful, like in this case a URL for the LLM to link to.
|
||||
const featureFlagWithUrl = {
|
||||
...flagResult.data,
|
||||
url: `${context.api.getProjectBaseUrl(projectId)}/feature_flags/${flagResult.data.id}`,
|
||||
}
|
||||
// Add context that is useful, like in this case a URL for the LLM to link to.
|
||||
const featureFlagWithUrl = {
|
||||
...flagResult.data,
|
||||
url: `${context.api.getProjectBaseUrl(projectId)}/feature_flags/${flagResult.data.id}`,
|
||||
}
|
||||
|
||||
return {
|
||||
content: [{ type: 'text', text: JSON.stringify(featureFlagWithUrl) }],
|
||||
}
|
||||
return {
|
||||
content: [{ type: 'text', text: JSON.stringify(featureFlagWithUrl) }],
|
||||
}
|
||||
}
|
||||
|
||||
const tool = (): ToolBase<typeof schema> => ({
|
||||
name: 'create-feature-flag',
|
||||
schema,
|
||||
handler: createHandler,
|
||||
name: 'create-feature-flag',
|
||||
schema,
|
||||
handler: createHandler,
|
||||
})
|
||||
|
||||
export default tool
|
||||
@@ -95,20 +96,20 @@ Add a clear, actionable description for your tool, assign it to a feature, speci
|
||||
|
||||
```json
|
||||
{
|
||||
"create-feature-flag": {
|
||||
"title": "Create Feature Flag",
|
||||
"description": "Creates a new feature flag in the project. Once you have created a feature flag, you should: Ask the user if they want to add it to their codebase, Use the \"search-docs\" tool to find documentation on how to add feature flags to the codebase (search for the right language / framework), Clarify where it should be added and then add it.",
|
||||
"category": "Feature flags", // This will be displayed in the docs, but not readable by the MCP client
|
||||
"feature": "flags",
|
||||
"summary": "Creates a new feature flag in the project.", // This will be displayed in the docs, but not readable by the MCP client.
|
||||
"required_scopes": ["feature_flag:write"], // You can find a list of available scopes here: https://github.com/PostHog/posthog/blob/31082f4bcc4c45a0ac830777b8a3048e7752a1bc/frontend/src/lib/scopes.tsx
|
||||
"annotations": {
|
||||
"destructiveHint": false, // Does the tool delete or destructively modify data?
|
||||
"idempotentHint": false, // Can the tool be safely called multiple times with same result?
|
||||
"openWorldHint": true, // Does the tool interact with external systems or create new resources?
|
||||
"readOnlyHint": false // Is the tool read-only (doesn't modify any state)?
|
||||
}
|
||||
"create-feature-flag": {
|
||||
"title": "Create Feature Flag",
|
||||
"description": "Creates a new feature flag in the project. Once you have created a feature flag, you should: Ask the user if they want to add it to their codebase, Use the \"search-docs\" tool to find documentation on how to add feature flags to the codebase (search for the right language / framework), Clarify where it should be added and then add it.",
|
||||
"category": "Feature flags", // This will be displayed in the docs, but not readable by the MCP client
|
||||
"feature": "flags",
|
||||
"summary": "Creates a new feature flag in the project.", // This will be displayed in the docs, but not readable by the MCP client.
|
||||
"required_scopes": ["feature_flag:write"], // You can find a list of available scopes here: https://github.com/PostHog/posthog/blob/31082f4bcc4c45a0ac830777b8a3048e7752a1bc/frontend/src/lib/scopes.tsx
|
||||
"annotations": {
|
||||
"destructiveHint": false, // Does the tool delete or destructively modify data?
|
||||
"idempotentHint": false, // Can the tool be safely called multiple times with same result?
|
||||
"openWorldHint": true, // Does the tool interact with external systems or create new resources?
|
||||
"readOnlyHint": false // Is the tool read-only (doesn't modify any state)?
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -140,63 +141,64 @@ If you do add a new feature, make sure to update the `README.md` in the root of
|
||||
Always include integration tests to help us catch if there is a change to the underlying API:
|
||||
|
||||
```typescript
|
||||
import {
|
||||
cleanupResources,
|
||||
createTestClient,
|
||||
createTestContext,
|
||||
generateUniqueKey,
|
||||
parseToolResponse,
|
||||
setActiveProjectAndOrg,
|
||||
} from '@/shared/test-utils'
|
||||
import createFeatureFlagTool from '@/tools/featureFlags/create'
|
||||
import { afterEach, beforeAll, describe, expect, it } from 'vitest'
|
||||
|
||||
import {
|
||||
cleanupResources,
|
||||
createTestClient,
|
||||
createTestContext,
|
||||
generateUniqueKey,
|
||||
parseToolResponse,
|
||||
setActiveProjectAndOrg,
|
||||
} from '@/shared/test-utils'
|
||||
import createFeatureFlagTool from '@/tools/featureFlags/create'
|
||||
|
||||
describe('Feature Flags', () => {
|
||||
let context: Context
|
||||
const createdResources: CreatedResources = {
|
||||
featureFlags: [],
|
||||
insights: [],
|
||||
dashboards: [],
|
||||
}
|
||||
let context: Context
|
||||
const createdResources: CreatedResources = {
|
||||
featureFlags: [],
|
||||
insights: [],
|
||||
dashboards: [],
|
||||
}
|
||||
|
||||
beforeAll(async () => {
|
||||
const client = createTestClient()
|
||||
context = createTestContext(client)
|
||||
await setActiveProjectAndOrg(context, TEST_PROJECT_ID!, TEST_ORG_ID!)
|
||||
beforeAll(async () => {
|
||||
const client = createTestClient()
|
||||
context = createTestContext(client)
|
||||
await setActiveProjectAndOrg(context, TEST_PROJECT_ID!, TEST_ORG_ID!)
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
await cleanupResources(context.api, TEST_PROJECT_ID!, createdResources)
|
||||
})
|
||||
|
||||
describe('create-feature-flag tool', () => {
|
||||
const createTool = createFeatureFlagTool()
|
||||
|
||||
it('should create a feature flag with minimal required fields', async () => {
|
||||
const params = {
|
||||
name: 'Test Feature Flag',
|
||||
key: generateUniqueKey('test-flag'),
|
||||
description: 'Integration test flag',
|
||||
filters: { groups: [] },
|
||||
active: true,
|
||||
}
|
||||
|
||||
const result = await createTool.handler(context, params)
|
||||
const flagData = parseToolResponse(result)
|
||||
|
||||
expect(flagData.id).toBeTruthy()
|
||||
expect(flagData.key).toBe(params.key)
|
||||
expect(flagData.name).toBe(params.name)
|
||||
expect(flagData.active).toBe(params.active)
|
||||
expect(flagData.url).toContain('/feature_flags/')
|
||||
|
||||
createdResources.featureFlags.push(flagData.id)
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
await cleanupResources(context.api, TEST_PROJECT_ID!, createdResources)
|
||||
})
|
||||
|
||||
describe('create-feature-flag tool', () => {
|
||||
const createTool = createFeatureFlagTool()
|
||||
|
||||
it('should create a feature flag with minimal required fields', async () => {
|
||||
const params = {
|
||||
name: 'Test Feature Flag',
|
||||
key: generateUniqueKey('test-flag'),
|
||||
description: 'Integration test flag',
|
||||
filters: { groups: [] },
|
||||
active: true,
|
||||
}
|
||||
|
||||
const result = await createTool.handler(context, params)
|
||||
const flagData = parseToolResponse(result)
|
||||
|
||||
expect(flagData.id).toBeDefined()
|
||||
expect(flagData.key).toBe(params.key)
|
||||
expect(flagData.name).toBe(params.name)
|
||||
expect(flagData.active).toBe(params.active)
|
||||
expect(flagData.url).toContain('/feature_flags/')
|
||||
|
||||
createdResources.featureFlags.push(flagData.id)
|
||||
})
|
||||
|
||||
it('should create a feature flag with complex filters', async () => {
|
||||
// Test with more complex scenarios
|
||||
})
|
||||
it('should create a feature flag with complex filters', async () => {
|
||||
// Test with more complex scenarios
|
||||
})
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
|
||||
@@ -1,21 +1,20 @@
|
||||
import type { z } from 'zod'
|
||||
|
||||
import { DashboardAddInsightSchema } from '@/schema/tool-inputs'
|
||||
import { resolveInsightId } from '@/tools/insights/utils'
|
||||
import type { Context, ToolBase } from '@/tools/types'
|
||||
import type { z } from 'zod'
|
||||
|
||||
const schema = DashboardAddInsightSchema
|
||||
|
||||
type Params = z.infer<typeof schema>
|
||||
|
||||
export const addInsightHandler = async (context: Context, params: Params) => {
|
||||
export const addInsightHandler: ToolBase<typeof schema>['handler'] = async (context: Context, params: Params) => {
|
||||
const { data } = params
|
||||
const projectId = await context.stateManager.getProjectId()
|
||||
|
||||
const numericInsightId = await resolveInsightId(context, data.insightId, projectId)
|
||||
|
||||
const insightResult = await context.api
|
||||
.insights({ projectId })
|
||||
.get({ insightId: data.insightId })
|
||||
const insightResult = await context.api.insights({ projectId }).get({ insightId: data.insightId })
|
||||
|
||||
if (!insightResult.success) {
|
||||
throw new Error(`Failed to get insight: ${insightResult.error.message}`)
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import type { z } from 'zod'
|
||||
|
||||
import { DashboardCreateSchema } from '@/schema/tool-inputs'
|
||||
import type { Context, ToolBase } from '@/tools/types'
|
||||
import type { z } from 'zod'
|
||||
|
||||
const schema = DashboardCreateSchema
|
||||
|
||||
type Params = z.infer<typeof schema>
|
||||
|
||||
export const createHandler = async (context: Context, params: Params) => {
|
||||
export const createHandler: ToolBase<typeof schema>['handler'] = async (context: Context, params: Params) => {
|
||||
const { data } = params
|
||||
const projectId = await context.stateManager.getProjectId()
|
||||
const dashboardResult = await context.api.dashboards({ projectId }).create({ data })
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import type { z } from 'zod'
|
||||
|
||||
import { DashboardDeleteSchema } from '@/schema/tool-inputs'
|
||||
import type { Context, ToolBase } from '@/tools/types'
|
||||
import type { z } from 'zod'
|
||||
|
||||
const schema = DashboardDeleteSchema
|
||||
|
||||
type Params = z.infer<typeof schema>
|
||||
|
||||
export const deleteHandler = async (context: Context, params: Params) => {
|
||||
export const deleteHandler: ToolBase<typeof schema>['handler'] = async (context: Context, params: Params) => {
|
||||
const { dashboardId } = params
|
||||
const projectId = await context.stateManager.getProjectId()
|
||||
const result = await context.api.dashboards({ projectId }).delete({ dashboardId })
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import type { z } from 'zod'
|
||||
|
||||
import { DashboardGetSchema } from '@/schema/tool-inputs'
|
||||
import type { Context, ToolBase } from '@/tools/types'
|
||||
import type { z } from 'zod'
|
||||
|
||||
const schema = DashboardGetSchema
|
||||
|
||||
type Params = z.infer<typeof schema>
|
||||
|
||||
export const getHandler = async (context: Context, params: Params) => {
|
||||
export const getHandler: ToolBase<typeof schema>['handler'] = async (context: Context, params: Params) => {
|
||||
const { dashboardId } = params
|
||||
const projectId = await context.stateManager.getProjectId()
|
||||
const dashboardResult = await context.api.dashboards({ projectId }).get({ dashboardId })
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
import type { z } from 'zod'
|
||||
|
||||
import { DashboardGetAllSchema } from '@/schema/tool-inputs'
|
||||
import type { Context, ToolBase } from '@/tools/types'
|
||||
import type { z } from 'zod'
|
||||
|
||||
const schema = DashboardGetAllSchema
|
||||
|
||||
type Params = z.infer<typeof schema>
|
||||
|
||||
export const getAllHandler = async (context: Context, params: Params) => {
|
||||
export const getAllHandler: ToolBase<typeof schema>['handler'] = async (context: Context, params: Params) => {
|
||||
const { data } = params
|
||||
const projectId = await context.stateManager.getProjectId()
|
||||
const dashboardsResult = await context.api
|
||||
.dashboards({ projectId })
|
||||
.list({ params: data ?? {} })
|
||||
const dashboardsResult = await context.api.dashboards({ projectId }).list({ params: data ?? {} })
|
||||
|
||||
if (!dashboardsResult.success) {
|
||||
throw new Error(`Failed to get dashboards: ${dashboardsResult.error.message}`)
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
import type { z } from 'zod'
|
||||
|
||||
import { DashboardUpdateSchema } from '@/schema/tool-inputs'
|
||||
import type { Context, ToolBase } from '@/tools/types'
|
||||
import type { z } from 'zod'
|
||||
|
||||
const schema = DashboardUpdateSchema
|
||||
|
||||
type Params = z.infer<typeof schema>
|
||||
|
||||
export const updateHandler = async (context: Context, params: Params) => {
|
||||
export const updateHandler: ToolBase<typeof schema>['handler'] = async (context: Context, params: Params) => {
|
||||
const { dashboardId, data } = params
|
||||
const projectId = await context.stateManager.getProjectId()
|
||||
const dashboardResult = await context.api
|
||||
.dashboards({ projectId })
|
||||
.update({ dashboardId, data })
|
||||
const dashboardResult = await context.api.dashboards({ projectId }).update({ dashboardId, data })
|
||||
|
||||
if (!dashboardResult.success) {
|
||||
throw new Error(`Failed to update dashboard: ${dashboardResult.error.message}`)
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import type { z } from 'zod'
|
||||
|
||||
import { docsSearch } from '@/inkeepApi'
|
||||
import { DocumentationSearchSchema } from '@/schema/tool-inputs'
|
||||
import type { Context, ToolBase } from '@/tools/types'
|
||||
import type { z } from 'zod'
|
||||
|
||||
const schema = DocumentationSearchSchema
|
||||
|
||||
type Params = z.infer<typeof schema>
|
||||
|
||||
export const searchDocsHandler = async (context: Context, params: Params) => {
|
||||
export const searchDocsHandler: ToolBase<typeof schema>['handler'] = async (context: Context, params: Params) => {
|
||||
const { query } = params
|
||||
const inkeepApiKey = context.env.INKEEP_API_KEY
|
||||
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import type { z } from 'zod'
|
||||
|
||||
import { ErrorTrackingDetailsSchema } from '@/schema/tool-inputs'
|
||||
import type { Context, ToolBase } from '@/tools/types'
|
||||
import type { z } from 'zod'
|
||||
|
||||
const schema = ErrorTrackingDetailsSchema
|
||||
|
||||
type Params = z.infer<typeof schema>
|
||||
|
||||
export const errorDetailsHandler = async (context: Context, params: Params) => {
|
||||
export const errorDetailsHandler: ToolBase<typeof schema>['handler'] = async (context: Context, params: Params) => {
|
||||
const { issueId, dateFrom, dateTo } = params
|
||||
const projectId = await context.stateManager.getProjectId()
|
||||
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import type { z } from 'zod'
|
||||
|
||||
import { ErrorTrackingListSchema } from '@/schema/tool-inputs'
|
||||
import type { Context, ToolBase } from '@/tools/types'
|
||||
import type { z } from 'zod'
|
||||
|
||||
const schema = ErrorTrackingListSchema
|
||||
|
||||
type Params = z.infer<typeof schema>
|
||||
|
||||
export const listErrorsHandler = async (context: Context, params: Params) => {
|
||||
export const listErrorsHandler: ToolBase<typeof schema>['handler'] = async (context: Context, params: Params) => {
|
||||
const { orderBy, dateFrom, dateTo, orderDirection, filterTestAccounts, status } = params
|
||||
const projectId = await context.stateManager.getProjectId()
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { z } from 'zod'
|
||||
|
||||
import { ExperimentCreateSchema } from '@/schema/tool-inputs'
|
||||
import type { Context, ToolBase } from '@/tools/types'
|
||||
import type { z } from 'zod'
|
||||
|
||||
const schema = ExperimentCreateSchema
|
||||
|
||||
@@ -10,7 +11,7 @@ type Params = z.infer<typeof schema>
|
||||
* Create a comprehensive A/B test experiment with guided setup
|
||||
* This tool helps users create well-configured experiments through conversation
|
||||
*/
|
||||
export const createExperimentHandler = async (context: Context, params: Params) => {
|
||||
export const createExperimentHandler: ToolBase<typeof schema>['handler'] = async (context: Context, params: Params) => {
|
||||
const projectId = await context.stateManager.getProjectId()
|
||||
|
||||
const result = await context.api.experiments({ projectId }).create(params)
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import type { z } from 'zod'
|
||||
|
||||
import { ExperimentDeleteSchema } from '@/schema/tool-inputs'
|
||||
import type { Context, ToolBase } from '@/tools/types'
|
||||
import type { z } from 'zod'
|
||||
|
||||
const schema = ExperimentDeleteSchema
|
||||
|
||||
type Params = z.infer<typeof schema>
|
||||
|
||||
export const deleteHandler = async (context: Context, { experimentId }: Params) => {
|
||||
export const deleteHandler: ToolBase<typeof schema>['handler'] = async (context: Context, { experimentId }: Params) => {
|
||||
const projectId = await context.stateManager.getProjectId()
|
||||
|
||||
const deleteResult = await context.api.experiments({ projectId }).delete({
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import type { z } from 'zod'
|
||||
|
||||
import { ExperimentGetSchema } from '@/schema/tool-inputs'
|
||||
import type { Context, ToolBase } from '@/tools/types'
|
||||
import type { z } from 'zod'
|
||||
|
||||
const schema = ExperimentGetSchema
|
||||
|
||||
type Params = z.infer<typeof schema>
|
||||
|
||||
export const getHandler = async (context: Context, { experimentId }: Params) => {
|
||||
export const getHandler: ToolBase<typeof schema>['handler'] = async (context: Context, { experimentId }: Params) => {
|
||||
const projectId = await context.stateManager.getProjectId()
|
||||
|
||||
const result = await context.api.experiments({ projectId }).get({
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
import { ExperimentGetAllSchema } from '@/schema/tool-inputs'
|
||||
import type { Context, ToolBase } from '@/tools/types'
|
||||
import type { z } from 'zod'
|
||||
|
||||
const schema = ExperimentGetAllSchema
|
||||
|
||||
type Params = z.infer<typeof schema>
|
||||
|
||||
export const getAllHandler = async (context: Context, _params: Params) => {
|
||||
export const getAllHandler: ToolBase<typeof schema>['handler'] = async (context: Context) => {
|
||||
const projectId = await context.stateManager.getProjectId()
|
||||
|
||||
const results = await context.api.experiments({ projectId }).list()
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import type { z } from 'zod'
|
||||
|
||||
import { ExperimentResultsResponseSchema } from '@/schema/experiments'
|
||||
import { ExperimentResultsGetSchema } from '@/schema/tool-inputs'
|
||||
import type { Context, ToolBase } from '@/tools/types'
|
||||
import type { z } from 'zod'
|
||||
|
||||
const schema = ExperimentResultsGetSchema
|
||||
|
||||
@@ -12,7 +13,7 @@ type Params = z.infer<typeof schema>
|
||||
* This tool fetches the experiment details and executes the necessary queries
|
||||
* to get metrics results (both primary and secondary) and exposure data
|
||||
*/
|
||||
export const getResultsHandler = async (context: Context, params: Params) => {
|
||||
export const getResultsHandler: ToolBase<typeof schema>['handler'] = async (context: Context, params: Params) => {
|
||||
const projectId = await context.stateManager.getProjectId()
|
||||
|
||||
const result = await context.api.experiments({ projectId }).getMetricResults({
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import type { z } from 'zod'
|
||||
|
||||
import { ExperimentUpdateTransformSchema } from '@/schema/experiments'
|
||||
import { ExperimentUpdateSchema } from '@/schema/tool-inputs'
|
||||
import { getToolDefinition } from '@/tools/toolDefinitions'
|
||||
import type { Context, Tool } from '@/tools/types'
|
||||
import type { z } from 'zod'
|
||||
import type { Context, Tool, ToolBase } from '@/tools/types'
|
||||
|
||||
const schema = ExperimentUpdateSchema
|
||||
|
||||
type Params = z.infer<typeof schema>
|
||||
|
||||
export const updateHandler = async (context: Context, params: Params) => {
|
||||
export const updateHandler: ToolBase<typeof schema>['handler'] = async (context: Context, params: Params) => {
|
||||
const { experimentId, data } = params
|
||||
const projectId = await context.stateManager.getProjectId()
|
||||
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import type { z } from 'zod'
|
||||
|
||||
import { FeatureFlagCreateSchema } from '@/schema/tool-inputs'
|
||||
import type { Context, ToolBase } from '@/tools/types'
|
||||
import type { z } from 'zod'
|
||||
|
||||
const schema = FeatureFlagCreateSchema
|
||||
|
||||
type Params = z.infer<typeof schema>
|
||||
|
||||
export const createHandler = async (context: Context, params: Params) => {
|
||||
export const createHandler: ToolBase<typeof schema>['handler'] = async (context: Context, params: Params) => {
|
||||
const { name, key, description, filters, active, tags } = params
|
||||
const projectId = await context.stateManager.getProjectId()
|
||||
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import type { z } from 'zod'
|
||||
|
||||
import { FeatureFlagDeleteSchema } from '@/schema/tool-inputs'
|
||||
import type { Context, ToolBase } from '@/tools/types'
|
||||
import type { z } from 'zod'
|
||||
|
||||
const schema = FeatureFlagDeleteSchema
|
||||
|
||||
type Params = z.infer<typeof schema>
|
||||
|
||||
export const deleteHandler = async (context: Context, params: Params) => {
|
||||
export const deleteHandler: ToolBase<typeof schema>['handler'] = async (context: Context, params: Params) => {
|
||||
const { flagKey } = params
|
||||
const projectId = await context.stateManager.getProjectId()
|
||||
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
import { FeatureFlagGetAllSchema } from '@/schema/tool-inputs'
|
||||
import type { Context, ToolBase } from '@/tools/types'
|
||||
import type { z } from 'zod'
|
||||
|
||||
const schema = FeatureFlagGetAllSchema
|
||||
|
||||
type Params = z.infer<typeof schema>
|
||||
|
||||
export const getAllHandler = async (context: Context, _params: Params) => {
|
||||
export const getAllHandler: ToolBase<typeof schema>['handler'] = async (context: Context) => {
|
||||
const projectId = await context.stateManager.getProjectId()
|
||||
|
||||
const flagsResult = await context.api.featureFlags({ projectId }).list()
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
import type { z } from 'zod'
|
||||
|
||||
import { FeatureFlagGetDefinitionSchema } from '@/schema/tool-inputs'
|
||||
import type { Context, ToolBase } from '@/tools/types'
|
||||
import type { z } from 'zod'
|
||||
|
||||
const schema = FeatureFlagGetDefinitionSchema
|
||||
|
||||
type Params = z.infer<typeof schema>
|
||||
|
||||
export const getDefinitionHandler = async (context: Context, { flagId, flagKey }: Params) => {
|
||||
export const getDefinitionHandler: ToolBase<typeof schema>['handler'] = async (
|
||||
context: Context,
|
||||
{ flagId, flagKey }: Params
|
||||
) => {
|
||||
if (!flagId && !flagKey) {
|
||||
return {
|
||||
content: [
|
||||
@@ -21,9 +25,7 @@ export const getDefinitionHandler = async (context: Context, { flagId, flagKey }
|
||||
const projectId = await context.stateManager.getProjectId()
|
||||
|
||||
if (flagId) {
|
||||
const flagResult = await context.api
|
||||
.featureFlags({ projectId })
|
||||
.get({ flagId: String(flagId) })
|
||||
const flagResult = await context.api.featureFlags({ projectId }).get({ flagId: String(flagId) })
|
||||
if (!flagResult.success) {
|
||||
throw new Error(`Failed to get feature flag: ${flagResult.error.message}`)
|
||||
}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import type { z } from 'zod'
|
||||
|
||||
import { FeatureFlagUpdateSchema } from '@/schema/tool-inputs'
|
||||
import type { Context, ToolBase } from '@/tools/types'
|
||||
import type { z } from 'zod'
|
||||
|
||||
const schema = FeatureFlagUpdateSchema
|
||||
|
||||
type Params = z.infer<typeof schema>
|
||||
|
||||
export const updateHandler = async (context: Context, params: Params) => {
|
||||
export const updateHandler: ToolBase<typeof schema>['handler'] = async (context: Context, params: Params) => {
|
||||
const { flagKey, data } = params
|
||||
const projectId = await context.stateManager.getProjectId()
|
||||
|
||||
|
||||
@@ -1,37 +1,22 @@
|
||||
import type { Context, Tool, ToolBase, ZodObjectAny } from './types'
|
||||
|
||||
import { ApiClient } from '@/api/client'
|
||||
import { SessionManager } from '@/lib/utils/SessionManager'
|
||||
import { StateManager } from '@/lib/utils/StateManager'
|
||||
import { hasScopes } from '@/lib/utils/api'
|
||||
import { MemoryCache } from '@/lib/utils/cache/MemoryCache'
|
||||
import { hash } from '@/lib/utils/helper-functions'
|
||||
import { getToolsForFeatures as getFilteredToolNames, getToolDefinition } from './toolDefinitions'
|
||||
|
||||
import createFeatureFlag from './featureFlags/create'
|
||||
import deleteFeatureFlag from './featureFlags/delete'
|
||||
import getAllFeatureFlags from './featureFlags/getAll'
|
||||
// Feature Flags
|
||||
import getFeatureFlagDefinition from './featureFlags/getDefinition'
|
||||
import updateFeatureFlag from './featureFlags/update'
|
||||
|
||||
import getOrganizationDetails from './organizations/getDetails'
|
||||
// Organizations
|
||||
import getOrganizations from './organizations/getOrganizations'
|
||||
import setActiveOrganization from './organizations/setActive'
|
||||
|
||||
import eventDefinitions from './projects/eventDefinitions'
|
||||
// Projects
|
||||
import getProjects from './projects/getProjects'
|
||||
import getProperties from './projects/propertyDefinitions'
|
||||
import setActiveProject from './projects/setActive'
|
||||
|
||||
// Dashboards
|
||||
import addInsightToDashboard from './dashboards/addInsight'
|
||||
import createDashboard from './dashboards/create'
|
||||
import deleteDashboard from './dashboards/delete'
|
||||
import getDashboard from './dashboards/get'
|
||||
import getAllDashboards from './dashboards/getAll'
|
||||
import updateDashboard from './dashboards/update'
|
||||
// Documentation
|
||||
import searchDocs from './documentation/searchDocs'
|
||||
|
||||
import errorDetails from './errorTracking/errorDetails'
|
||||
// Error Tracking
|
||||
import errorDetails from './errorTracking/errorDetails'
|
||||
import listErrors from './errorTracking/listErrors'
|
||||
|
||||
// Experiments
|
||||
import createExperiment from './experiments/create'
|
||||
import deleteExperiment from './experiments/delete'
|
||||
@@ -39,32 +24,33 @@ import getExperiment from './experiments/get'
|
||||
import getAllExperiments from './experiments/getAll'
|
||||
import getExperimentResults from './experiments/getResults'
|
||||
import updateExperiment from './experiments/update'
|
||||
|
||||
// Feature Flags
|
||||
import createFeatureFlag from './featureFlags/create'
|
||||
import deleteFeatureFlag from './featureFlags/delete'
|
||||
import getAllFeatureFlags from './featureFlags/getAll'
|
||||
import getFeatureFlagDefinition from './featureFlags/getDefinition'
|
||||
import updateFeatureFlag from './featureFlags/update'
|
||||
// Insights
|
||||
import createInsight from './insights/create'
|
||||
import deleteInsight from './insights/delete'
|
||||
|
||||
import getInsight from './insights/get'
|
||||
// Insights
|
||||
import getAllInsights from './insights/getAll'
|
||||
import queryInsight from './insights/query'
|
||||
import updateInsight from './insights/update'
|
||||
|
||||
import addInsightToDashboard from './dashboards/addInsight'
|
||||
import createDashboard from './dashboards/create'
|
||||
import deleteDashboard from './dashboards/delete'
|
||||
import getDashboard from './dashboards/get'
|
||||
|
||||
// Dashboards
|
||||
import getAllDashboards from './dashboards/getAll'
|
||||
import updateDashboard from './dashboards/update'
|
||||
import generateHogQLFromQuestion from './query/generateHogQLFromQuestion'
|
||||
// Query
|
||||
import queryRun from './query/run'
|
||||
|
||||
import { hasScopes } from '@/lib/utils/api'
|
||||
// LLM Observability
|
||||
import getLLMCosts from './llmAnalytics/getLLMCosts'
|
||||
|
||||
// Organizations
|
||||
import getOrganizationDetails from './organizations/getDetails'
|
||||
import getOrganizations from './organizations/getOrganizations'
|
||||
import setActiveOrganization from './organizations/setActive'
|
||||
// Projects
|
||||
import eventDefinitions from './projects/eventDefinitions'
|
||||
import getProjects from './projects/getProjects'
|
||||
import getProperties from './projects/propertyDefinitions'
|
||||
import setActiveProject from './projects/setActive'
|
||||
// Query
|
||||
import generateHogQLFromQuestion from './query/generateHogQLFromQuestion'
|
||||
import queryRun from './query/run'
|
||||
// Surveys
|
||||
import createSurvey from './surveys/create'
|
||||
import deleteSurvey from './surveys/delete'
|
||||
@@ -73,6 +59,9 @@ import getAllSurveys from './surveys/getAll'
|
||||
import surveysGlobalStats from './surveys/global-stats'
|
||||
import surveyStats from './surveys/stats'
|
||||
import updateSurvey from './surveys/update'
|
||||
// Misc
|
||||
import { getToolsForFeatures as getFilteredToolNames, getToolDefinition } from './toolDefinitions'
|
||||
import type { Context, Tool, ToolBase, ZodObjectAny } from './types'
|
||||
|
||||
// Map of tool names to tool factory functions
|
||||
const TOOL_MAP: Record<string, () => ToolBase<ZodObjectAny>> = {
|
||||
@@ -142,10 +131,7 @@ const TOOL_MAP: Record<string, () => ToolBase<ZodObjectAny>> = {
|
||||
'survey-stats': surveyStats,
|
||||
}
|
||||
|
||||
export const getToolsFromContext = async (
|
||||
context: Context,
|
||||
features?: string[]
|
||||
): Promise<Tool<ZodObjectAny>[]> => {
|
||||
export const getToolsFromContext = async (context: Context, features?: string[]): Promise<Tool<ZodObjectAny>[]> => {
|
||||
const allowedToolNames = getFilteredToolNames(features)
|
||||
const toolBases: ToolBase<ZodObjectAny>[] = []
|
||||
|
||||
@@ -173,14 +159,14 @@ export const getToolsFromContext = async (
|
||||
})
|
||||
|
||||
try {
|
||||
const { scopes } = await context.stateManager.getApiKey()
|
||||
const { scopes } = (await context.stateManager.getApiKey())!
|
||||
|
||||
const filteredTools = tools.filter((tool) => {
|
||||
return hasScopes(scopes, tool.scopes)
|
||||
})
|
||||
|
||||
return filteredTools
|
||||
} catch (error) {
|
||||
} catch {
|
||||
// OAuth tokens don't support /api/personal_api_keys/@current endpoint yet
|
||||
// Return empty tool set until backend OAuth support is added
|
||||
return []
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import type { z } from 'zod'
|
||||
|
||||
import { InsightCreateSchema } from '@/schema/tool-inputs'
|
||||
import type { Context, ToolBase } from '@/tools/types'
|
||||
import type { z } from 'zod'
|
||||
|
||||
const schema = InsightCreateSchema
|
||||
|
||||
type Params = z.infer<typeof schema>
|
||||
|
||||
export const createHandler = async (context: Context, params: Params) => {
|
||||
export const createHandler: ToolBase<typeof schema>['handler'] = async (context: Context, params: Params) => {
|
||||
const { data } = params
|
||||
const projectId = await context.stateManager.getProjectId()
|
||||
const insightResult = await context.api.insights({ projectId }).create({ data })
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import type { z } from 'zod'
|
||||
|
||||
import { InsightDeleteSchema } from '@/schema/tool-inputs'
|
||||
import type { Context, ToolBase } from '@/tools/types'
|
||||
|
||||
import { resolveInsightId } from './utils'
|
||||
import type { z } from 'zod'
|
||||
|
||||
const schema = InsightDeleteSchema
|
||||
|
||||
type Params = z.infer<typeof schema>
|
||||
|
||||
export const deleteHandler = async (context: Context, params: Params) => {
|
||||
export const deleteHandler: ToolBase<typeof schema>['handler'] = async (context: Context, params: Params) => {
|
||||
const { insightId } = params
|
||||
const projectId = await context.stateManager.getProjectId()
|
||||
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import type { z } from 'zod'
|
||||
|
||||
import { InsightGetSchema } from '@/schema/tool-inputs'
|
||||
import type { Context, ToolBase } from '@/tools/types'
|
||||
import type { z } from 'zod'
|
||||
|
||||
const schema = InsightGetSchema
|
||||
|
||||
type Params = z.infer<typeof schema>
|
||||
|
||||
export const getHandler = async (context: Context, params: Params) => {
|
||||
export const getHandler: ToolBase<typeof schema>['handler'] = async (context: Context, params: Params) => {
|
||||
const { insightId } = params
|
||||
const projectId = await context.stateManager.getProjectId()
|
||||
const insightResult = await context.api.insights({ projectId }).get({ insightId })
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import type { z } from 'zod'
|
||||
|
||||
import { InsightGetAllSchema } from '@/schema/tool-inputs'
|
||||
import type { Context, ToolBase } from '@/tools/types'
|
||||
import type { z } from 'zod'
|
||||
|
||||
const schema = InsightGetAllSchema
|
||||
|
||||
type Params = z.infer<typeof schema>
|
||||
|
||||
export const getAllHandler = async (context: Context, params: Params) => {
|
||||
export const getAllHandler: ToolBase<typeof schema>['handler'] = async (context: Context, params: Params) => {
|
||||
const { data } = params
|
||||
const projectId = await context.stateManager.getProjectId()
|
||||
const insightsResult = await context.api.insights({ projectId }).list({ params: { ...data } })
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import type { z } from 'zod'
|
||||
|
||||
import { InsightQueryInputSchema } from '@/schema/tool-inputs'
|
||||
import type { Context, ToolBase } from '@/tools/types'
|
||||
import type { z } from 'zod'
|
||||
|
||||
const schema = InsightQueryInputSchema
|
||||
|
||||
type Params = z.infer<typeof schema>
|
||||
|
||||
export const queryHandler = async (context: Context, params: Params) => {
|
||||
export const queryHandler: ToolBase<typeof schema>['handler'] = async (context: Context, params: Params) => {
|
||||
const { insightId } = params
|
||||
const projectId = await context.stateManager.getProjectId()
|
||||
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import type { z } from 'zod'
|
||||
|
||||
import { InsightUpdateSchema } from '@/schema/tool-inputs'
|
||||
import type { Context, ToolBase } from '@/tools/types'
|
||||
import type { z } from 'zod'
|
||||
|
||||
import { resolveInsightId } from './utils'
|
||||
|
||||
const schema = InsightUpdateSchema
|
||||
|
||||
type Params = z.infer<typeof schema>
|
||||
|
||||
export const updateHandler = async (context: Context, params: Params) => {
|
||||
export const updateHandler: ToolBase<typeof schema>['handler'] = async (context: Context, params: Params) => {
|
||||
const { insightId, data } = params
|
||||
const projectId = await context.stateManager.getProjectId()
|
||||
|
||||
|
||||
@@ -4,11 +4,7 @@ export const isShortId = (id: string): boolean => {
|
||||
return /^[A-Za-z0-9]{8}$/.test(id)
|
||||
}
|
||||
|
||||
export const resolveInsightId = async (
|
||||
context: Context,
|
||||
insightId: string,
|
||||
projectId: string
|
||||
): Promise<number> => {
|
||||
export const resolveInsightId = async (context: Context, insightId: string, projectId: string): Promise<number> => {
|
||||
if (isShortId(insightId)) {
|
||||
const result = await context.api.insights({ projectId }).get({ insightId })
|
||||
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import type { z } from 'zod'
|
||||
|
||||
import { LLMAnalyticsGetCostsSchema } from '@/schema/tool-inputs'
|
||||
import type { Context, ToolBase } from '@/tools/types'
|
||||
import type { z } from 'zod'
|
||||
|
||||
const schema = LLMAnalyticsGetCostsSchema
|
||||
|
||||
type Params = z.infer<typeof schema>
|
||||
|
||||
export const getLLMCostsHandler = async (context: Context, params: Params) => {
|
||||
export const getLLMCostsHandler: ToolBase<typeof schema>['handler'] = async (context: Context, params: Params) => {
|
||||
const { projectId, days } = params
|
||||
|
||||
const trendsQuery = {
|
||||
@@ -31,9 +32,7 @@ export const getLLMCostsHandler = async (context: Context, params: Params) => {
|
||||
},
|
||||
}
|
||||
|
||||
const costsResult = await context.api
|
||||
.query({ projectId: String(projectId) })
|
||||
.execute({ queryBody: trendsQuery })
|
||||
const costsResult = await context.api.query({ projectId: String(projectId) }).execute({ queryBody: trendsQuery })
|
||||
if (!costsResult.success) {
|
||||
throw new Error(`Failed to get LLM costs: ${costsResult.error.message}`)
|
||||
}
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
import { OrganizationGetDetailsSchema } from '@/schema/tool-inputs'
|
||||
import type { Context, ToolBase } from '@/tools/types'
|
||||
import type { z } from 'zod'
|
||||
|
||||
const schema = OrganizationGetDetailsSchema
|
||||
|
||||
type Params = z.infer<typeof schema>
|
||||
|
||||
export const getDetailsHandler = async (context: Context, _params: Params) => {
|
||||
export const getDetailsHandler: ToolBase<typeof schema>['handler'] = async (context: Context) => {
|
||||
const orgId = await context.stateManager.getOrgID()
|
||||
|
||||
if (!orgId) {
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
import { OrganizationGetAllSchema } from '@/schema/tool-inputs'
|
||||
import type { Context, ToolBase } from '@/tools/types'
|
||||
import type { z } from 'zod'
|
||||
|
||||
const schema = OrganizationGetAllSchema
|
||||
|
||||
type Params = z.infer<typeof schema>
|
||||
|
||||
export const getOrganizationsHandler = async (context: Context, _params: Params) => {
|
||||
export const getOrganizationsHandler: ToolBase<typeof schema>['handler'] = async (context: Context) => {
|
||||
const orgsResult = await context.api.organizations().list()
|
||||
if (!orgsResult.success) {
|
||||
throw new Error(`Failed to get organizations: ${orgsResult.error.message}`)
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import type { z } from 'zod'
|
||||
|
||||
import { OrganizationSetActiveSchema } from '@/schema/tool-inputs'
|
||||
import type { Context, ToolBase } from '@/tools/types'
|
||||
import type { z } from 'zod'
|
||||
|
||||
const schema = OrganizationSetActiveSchema
|
||||
|
||||
type Params = z.infer<typeof schema>
|
||||
|
||||
export const setActiveHandler = async (context: Context, params: Params) => {
|
||||
export const setActiveHandler: ToolBase<typeof schema>['handler'] = async (context: Context, params: Params) => {
|
||||
const { orgId } = params
|
||||
await context.cache.set('orgId', orgId)
|
||||
|
||||
|
||||
@@ -1,18 +1,20 @@
|
||||
import type { z } from 'zod'
|
||||
|
||||
import { EventDefinitionSchema } from '@/schema/properties'
|
||||
import { ProjectEventDefinitionsSchema } from '@/schema/tool-inputs'
|
||||
import type { Context, ToolBase } from '@/tools/types'
|
||||
import type { z } from 'zod'
|
||||
|
||||
const schema = ProjectEventDefinitionsSchema
|
||||
|
||||
type Params = z.infer<typeof schema>
|
||||
|
||||
export const eventDefinitionsHandler = async (context: Context, _params: Params) => {
|
||||
export const eventDefinitionsHandler: ToolBase<typeof schema>['handler'] = async (
|
||||
context: Context,
|
||||
_params: Params
|
||||
) => {
|
||||
const projectId = await context.stateManager.getProjectId()
|
||||
|
||||
const eventDefsResult = await context.api
|
||||
.projects()
|
||||
.eventDefinitions({ projectId, search: _params.q })
|
||||
const eventDefsResult = await context.api.projects().eventDefinitions({ projectId, search: _params.q })
|
||||
|
||||
if (!eventDefsResult.success) {
|
||||
throw new Error(`Failed to get event definitions: ${eventDefsResult.error.message}`)
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
import { ProjectGetAllSchema } from '@/schema/tool-inputs'
|
||||
import type { Context, ToolBase } from '@/tools/types'
|
||||
import type { z } from 'zod'
|
||||
|
||||
const schema = ProjectGetAllSchema
|
||||
|
||||
type Params = z.infer<typeof schema>
|
||||
|
||||
export const getProjectsHandler = async (context: Context, _params: Params) => {
|
||||
export const getProjectsHandler: ToolBase<typeof schema>['handler'] = async (context: Context) => {
|
||||
const orgId = await context.stateManager.getOrgID()
|
||||
|
||||
if (!orgId) {
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
import type { z } from 'zod'
|
||||
|
||||
import { PropertyDefinitionSchema } from '@/schema/properties'
|
||||
import { ProjectPropertyDefinitionsInputSchema } from '@/schema/tool-inputs'
|
||||
import type { Context, ToolBase } from '@/tools/types'
|
||||
import type { z } from 'zod'
|
||||
|
||||
const schema = ProjectPropertyDefinitionsInputSchema
|
||||
|
||||
type Params = z.infer<typeof schema>
|
||||
|
||||
export const propertyDefinitionsHandler = async (context: Context, params: Params) => {
|
||||
export const propertyDefinitionsHandler: ToolBase<typeof schema>['handler'] = async (
|
||||
context: Context,
|
||||
params: Params
|
||||
) => {
|
||||
const projectId = await context.stateManager.getProjectId()
|
||||
|
||||
if (!params.eventName && params.type === 'event') {
|
||||
@@ -25,9 +29,7 @@ export const propertyDefinitionsHandler = async (context: Context, params: Param
|
||||
})
|
||||
|
||||
if (!propDefsResult.success) {
|
||||
throw new Error(
|
||||
`Failed to get property definitions for ${params.type}s: ${propDefsResult.error.message}`
|
||||
)
|
||||
throw new Error(`Failed to get property definitions for ${params.type}s: ${propDefsResult.error.message}`)
|
||||
}
|
||||
|
||||
const simplifiedProperties = PropertyDefinitionSchema.array().parse(propDefsResult.data)
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import type { z } from 'zod'
|
||||
|
||||
import { ProjectSetActiveSchema } from '@/schema/tool-inputs'
|
||||
import type { Context, ToolBase } from '@/tools/types'
|
||||
import type { z } from 'zod'
|
||||
|
||||
const schema = ProjectSetActiveSchema
|
||||
|
||||
type Params = z.infer<typeof schema>
|
||||
|
||||
export const setActiveHandler = async (context: Context, params: Params) => {
|
||||
export const setActiveHandler: ToolBase<typeof schema>['handler'] = async (context: Context, params: Params) => {
|
||||
const { projectId } = params
|
||||
|
||||
await context.cache.set('projectId', projectId.toString())
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import type { z } from 'zod'
|
||||
|
||||
import { InsightGenerateHogQLFromQuestionSchema } from '@/schema/tool-inputs'
|
||||
import type { Context, ToolBase } from '@/tools/types'
|
||||
import type { z } from 'zod'
|
||||
|
||||
const schema = InsightGenerateHogQLFromQuestionSchema
|
||||
|
||||
type Params = z.infer<typeof schema>
|
||||
|
||||
export const generateHogQLHandler = async (context: Context, params: Params) => {
|
||||
export const generateHogQLHandler: ToolBase<typeof schema>['handler'] = async (context: Context, params: Params) => {
|
||||
const { question } = params
|
||||
const projectId = await context.stateManager.getProjectId()
|
||||
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import type { z } from 'zod'
|
||||
|
||||
import { QueryRunInputSchema } from '@/schema/tool-inputs'
|
||||
import type { Context, ToolBase } from '@/tools/types'
|
||||
import type { z } from 'zod'
|
||||
|
||||
const schema = QueryRunInputSchema
|
||||
|
||||
type Params = z.infer<typeof schema>
|
||||
|
||||
export const queryRunHandler = async (context: Context, params: Params) => {
|
||||
export const queryRunHandler: ToolBase<typeof schema>['handler'] = async (context: Context, params: Params) => {
|
||||
const { query } = params
|
||||
|
||||
const projectId = await context.stateManager.getProjectId()
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import type { z } from 'zod'
|
||||
|
||||
import { SurveyCreateSchema } from '@/schema/tool-inputs'
|
||||
import { formatSurvey } from '@/tools/surveys/utils/survey-utils'
|
||||
import type { Context, ToolBase } from '@/tools/types'
|
||||
import type { z } from 'zod'
|
||||
|
||||
const schema = SurveyCreateSchema
|
||||
type Params = z.infer<typeof schema>
|
||||
|
||||
export const createHandler = async (context: Context, params: Params) => {
|
||||
export const createHandler: ToolBase<typeof schema>['handler'] = async (context: Context, params: Params) => {
|
||||
const projectId = await context.stateManager.getProjectId()
|
||||
|
||||
// Process questions to handle branching logic
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import type { z } from 'zod'
|
||||
|
||||
import { SurveyDeleteSchema } from '@/schema/tool-inputs'
|
||||
import type { Context, ToolBase } from '@/tools/types'
|
||||
import type { z } from 'zod'
|
||||
|
||||
const schema = SurveyDeleteSchema
|
||||
type Params = z.infer<typeof schema>
|
||||
|
||||
export const deleteHandler = async (context: Context, params: Params) => {
|
||||
export const deleteHandler: ToolBase<typeof schema>['handler'] = async (context: Context, params: Params) => {
|
||||
const { surveyId } = params
|
||||
const projectId = await context.stateManager.getProjectId()
|
||||
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import type { z } from 'zod'
|
||||
|
||||
import { SurveyGetSchema } from '@/schema/tool-inputs'
|
||||
import { formatSurvey } from '@/tools/surveys/utils/survey-utils'
|
||||
import type { Context, ToolBase } from '@/tools/types'
|
||||
import type { z } from 'zod'
|
||||
|
||||
const schema = SurveyGetSchema
|
||||
type Params = z.infer<typeof schema>
|
||||
|
||||
export const getHandler = async (context: Context, params: Params) => {
|
||||
export const getHandler: ToolBase<typeof schema>['handler'] = async (context: Context, params: Params) => {
|
||||
const { surveyId } = params
|
||||
const projectId = await context.stateManager.getProjectId()
|
||||
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import type { z } from 'zod'
|
||||
|
||||
import { SurveyGetAllSchema } from '@/schema/tool-inputs'
|
||||
import { formatSurveys } from '@/tools/surveys/utils/survey-utils'
|
||||
import type { Context, ToolBase } from '@/tools/types'
|
||||
import type { z } from 'zod'
|
||||
|
||||
const schema = SurveyGetAllSchema
|
||||
type Params = z.infer<typeof schema>
|
||||
|
||||
export const getAllHandler = async (context: Context, params: Params) => {
|
||||
export const getAllHandler: ToolBase<typeof schema>['handler'] = async (context: Context, params: Params) => {
|
||||
const projectId = await context.stateManager.getProjectId()
|
||||
|
||||
const surveysResult = await context.api.surveys({ projectId }).list(params ? { params } : {})
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import type { z } from 'zod'
|
||||
|
||||
import { SurveyGlobalStatsSchema } from '@/schema/tool-inputs'
|
||||
import type { Context, ToolBase } from '@/tools/types'
|
||||
import type { z } from 'zod'
|
||||
|
||||
const schema = SurveyGlobalStatsSchema
|
||||
type Params = z.infer<typeof schema>
|
||||
|
||||
export const globalStatsHandler = async (context: Context, params: Params) => {
|
||||
export const globalStatsHandler: ToolBase<typeof schema>['handler'] = async (context: Context, params: Params) => {
|
||||
const projectId = await context.stateManager.getProjectId()
|
||||
|
||||
const result = await context.api.surveys({ projectId }).globalStats({ params })
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import type { z } from 'zod'
|
||||
|
||||
import { SurveyStatsSchema } from '@/schema/tool-inputs'
|
||||
import type { Context, ToolBase } from '@/tools/types'
|
||||
import type { z } from 'zod'
|
||||
|
||||
const schema = SurveyStatsSchema
|
||||
type Params = z.infer<typeof schema>
|
||||
|
||||
export const statsHandler = async (context: Context, params: Params) => {
|
||||
export const statsHandler: ToolBase<typeof schema>['handler'] = async (context: Context, params: Params) => {
|
||||
const projectId = await context.stateManager.getProjectId()
|
||||
|
||||
const result = await context.api.surveys({ projectId }).stats({
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import type { z } from 'zod'
|
||||
|
||||
import { SurveyUpdateSchema } from '@/schema/tool-inputs'
|
||||
import { formatSurvey } from '@/tools/surveys/utils/survey-utils'
|
||||
import type { Context, ToolBase } from '@/tools/types'
|
||||
import type { z } from 'zod'
|
||||
|
||||
const schema = SurveyUpdateSchema
|
||||
type Params = z.infer<typeof schema>
|
||||
|
||||
export const updateHandler = async (context: Context, params: Params) => {
|
||||
export const updateHandler: ToolBase<typeof schema>['handler'] = async (context: Context, params: Params) => {
|
||||
const { surveyId, ...data } = params
|
||||
|
||||
const projectId = await context.stateManager.getProjectId()
|
||||
|
||||
@@ -12,11 +12,7 @@ export interface FormattedSurvey extends Omit<SurveyData, 'end_date'> {
|
||||
/**
|
||||
* Formats a survey with consistent status logic and additional fields
|
||||
*/
|
||||
export function formatSurvey(
|
||||
survey: SurveyData,
|
||||
context: Context,
|
||||
projectId: string
|
||||
): FormattedSurvey {
|
||||
export function formatSurvey(survey: SurveyData, context: Context, projectId: string): FormattedSurvey {
|
||||
const status = survey.archived
|
||||
? 'archived'
|
||||
: survey.start_date === null || survey.start_date === undefined
|
||||
@@ -43,10 +39,6 @@ export function formatSurvey(
|
||||
/**
|
||||
* Formats multiple surveys consistently
|
||||
*/
|
||||
export function formatSurveys(
|
||||
surveys: SurveyData[],
|
||||
context: Context,
|
||||
projectId: string
|
||||
): FormattedSurvey[] {
|
||||
export function formatSurveys(surveys: SurveyData[], context: Context, projectId: string): FormattedSurvey[] {
|
||||
return surveys.map((survey) => formatSurvey(survey, context, projectId))
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import z from 'zod'
|
||||
|
||||
import toolDefinitionsJson from '../../../schema/tool-definitions.json'
|
||||
|
||||
export const ToolDefinitionSchema = z.object({
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import type { z } from 'zod'
|
||||
|
||||
import type { ApiClient } from '@/api/client'
|
||||
import type { PrefixedString } from '@/lib/types'
|
||||
import type { StateManager } from '@/lib/utils/StateManager'
|
||||
import type { SessionManager } from '@/lib/utils/SessionManager'
|
||||
import type { StateManager } from '@/lib/utils/StateManager'
|
||||
import type { ScopedCache } from '@/lib/utils/cache/ScopedCache'
|
||||
import type { ApiRedactedPersonalApiKey } from '@/schema/api'
|
||||
import type { z } from 'zod'
|
||||
|
||||
export type CloudRegion = 'us' | 'eu'
|
||||
|
||||
|
||||
@@ -6,11 +6,11 @@ These tests should verify that the API works as intended.
|
||||
|
||||
1. **Configure environment:**
|
||||
|
||||
```bash
|
||||
cp .env.test.example .env.test
|
||||
```
|
||||
```bash
|
||||
cp .env.test.example .env.test
|
||||
```
|
||||
|
||||
Edit `.env.test` and set an api token and base url for your local PostHog instance
|
||||
Edit `.env.test` and set an api token and base url for your local PostHog instance
|
||||
|
||||
## Running Tests
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { afterEach, beforeAll, describe, expect, it } from 'vitest'
|
||||
|
||||
import { ApiClient } from '@/api/client'
|
||||
import type { CreateInsightInput } from '@/schema/insights'
|
||||
import { afterEach, beforeAll, describe, expect, it } from 'vitest'
|
||||
|
||||
const API_BASE_URL = process.env.TEST_POSTHOG_API_BASE_URL || 'http://localhost:8010'
|
||||
const API_TOKEN = process.env.TEST_POSTHOG_PERSONAL_API_KEY
|
||||
@@ -163,31 +164,28 @@ describe('API Client Integration Tests', { concurrent: false }, () => {
|
||||
}
|
||||
})
|
||||
|
||||
it.each(['event', 'person'] as const)(
|
||||
'should get property definitions for %s type',
|
||||
async (type) => {
|
||||
const result = await client.projects().propertyDefinitions({
|
||||
projectId: testProjectId,
|
||||
type,
|
||||
eventNames: type === 'event' ? ['$pageview'] : undefined,
|
||||
excludeCoreProperties: false,
|
||||
filterByEventNames: type === 'event',
|
||||
isFeatureFlag: false,
|
||||
limit: 100,
|
||||
})
|
||||
it.each(['event', 'person'] as const)('should get property definitions for %s type', async (type) => {
|
||||
const result = await client.projects().propertyDefinitions({
|
||||
projectId: testProjectId,
|
||||
type,
|
||||
eventNames: type === 'event' ? ['$pageview'] : undefined,
|
||||
excludeCoreProperties: false,
|
||||
filterByEventNames: type === 'event',
|
||||
isFeatureFlag: false,
|
||||
limit: 100,
|
||||
})
|
||||
|
||||
expect(result.success).toBe(true)
|
||||
expect(result.success).toBe(true)
|
||||
|
||||
if (result.success) {
|
||||
expect(Array.isArray(result.data)).toBe(true)
|
||||
if (result.data.length > 0) {
|
||||
const propDef = result.data[0]
|
||||
expect(propDef).toHaveProperty('id')
|
||||
expect(propDef).toHaveProperty('name')
|
||||
}
|
||||
if (result.success) {
|
||||
expect(Array.isArray(result.data)).toBe(true)
|
||||
if (result.data.length > 0) {
|
||||
const propDef = result.data[0]
|
||||
expect(propDef).toHaveProperty('id')
|
||||
expect(propDef).toHaveProperty('name')
|
||||
}
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
it('should get event definitions', async () => {
|
||||
const result = await client.projects().eventDefinitions({ projectId: testProjectId })
|
||||
@@ -279,9 +277,7 @@ describe('API Client Integration Tests', { concurrent: false }, () => {
|
||||
createdResources.featureFlags.push(flagId)
|
||||
|
||||
// Get by ID
|
||||
const getResult = await client
|
||||
.featureFlags({ projectId: testProjectId })
|
||||
.get({ flagId })
|
||||
const getResult = await client.featureFlags({ projectId: testProjectId }).get({ flagId })
|
||||
expect(getResult.success).toBe(true)
|
||||
|
||||
if (getResult.success) {
|
||||
@@ -290,9 +286,7 @@ describe('API Client Integration Tests', { concurrent: false }, () => {
|
||||
}
|
||||
|
||||
// Find by key
|
||||
const findResult = await client
|
||||
.featureFlags({ projectId: testProjectId })
|
||||
.findByKey({ key: testKey })
|
||||
const findResult = await client.featureFlags({ projectId: testProjectId }).findByKey({ key: testKey })
|
||||
expect(findResult.success).toBe(true)
|
||||
|
||||
if (findResult.success && findResult.data) {
|
||||
@@ -301,21 +295,17 @@ describe('API Client Integration Tests', { concurrent: false }, () => {
|
||||
}
|
||||
|
||||
// Update
|
||||
const updateResult = await client
|
||||
.featureFlags({ projectId: testProjectId })
|
||||
.update({
|
||||
key: testKey,
|
||||
data: {
|
||||
name: 'Updated Test Flag',
|
||||
active: false,
|
||||
},
|
||||
})
|
||||
const updateResult = await client.featureFlags({ projectId: testProjectId }).update({
|
||||
key: testKey,
|
||||
data: {
|
||||
name: 'Updated Test Flag',
|
||||
active: false,
|
||||
},
|
||||
})
|
||||
expect(updateResult.success).toBe(true)
|
||||
|
||||
// Verify update
|
||||
const updatedGetResult = await client
|
||||
.featureFlags({ projectId: testProjectId })
|
||||
.get({ flagId })
|
||||
const updatedGetResult = await client.featureFlags({ projectId: testProjectId }).get({ flagId })
|
||||
if (updatedGetResult.success) {
|
||||
expect(updatedGetResult.data.name).toBe('Updated Test Flag')
|
||||
expect(updatedGetResult.data.active).toBe(false)
|
||||
@@ -344,9 +334,7 @@ describe('API Client Integration Tests', { concurrent: false }, () => {
|
||||
})
|
||||
|
||||
it.skip('should execute SQL insight query', async () => {
|
||||
const result = await client
|
||||
.insights({ projectId: testProjectId })
|
||||
.sqlInsight({ query: 'SELECT 1 as test' })
|
||||
const result = await client.insights({ projectId: testProjectId }).sqlInsight({ query: 'SELECT 1 as test' })
|
||||
|
||||
if (!result.success) {
|
||||
console.error('Failed to execute SQL insight:', (result as any).error)
|
||||
@@ -523,8 +511,7 @@ describe('API Client Integration Tests', { concurrent: false }, () => {
|
||||
],
|
||||
breakdownFilter: {
|
||||
breakdown_type: breakdownType,
|
||||
breakdown:
|
||||
breakdownType === 'event' ? '$current_url' : '$browser',
|
||||
breakdown: breakdownType === 'event' ? '$current_url' : '$browser',
|
||||
breakdown_limit: 10,
|
||||
},
|
||||
properties: [],
|
||||
@@ -828,8 +815,7 @@ describe('API Client Integration Tests', { concurrent: false }, () => {
|
||||
},
|
||||
],
|
||||
funnelsFilter: {
|
||||
funnelWindowInterval:
|
||||
unit === 'minute' ? 30 : unit === 'hour' ? 2 : 7,
|
||||
funnelWindowInterval: unit === 'minute' ? 30 : unit === 'hour' ? 2 : 7,
|
||||
funnelWindowIntervalUnit: unit,
|
||||
},
|
||||
properties: [],
|
||||
@@ -1005,9 +991,7 @@ describe('API Client Integration Tests', { concurrent: false }, () => {
|
||||
createdResources.dashboards.push(dashboardId)
|
||||
|
||||
// Get
|
||||
const getResult = await client
|
||||
.dashboards({ projectId: testProjectId })
|
||||
.get({ dashboardId })
|
||||
const getResult = await client.dashboards({ projectId: testProjectId }).get({ dashboardId })
|
||||
expect(getResult.success).toBe(true)
|
||||
|
||||
if (getResult.success) {
|
||||
@@ -1043,9 +1027,7 @@ describe('API Client Integration Tests', { concurrent: false }, () => {
|
||||
status: 'active',
|
||||
}
|
||||
|
||||
const result = await client
|
||||
.query({ projectId: testProjectId })
|
||||
.execute({ queryBody: errorQuery })
|
||||
const result = await client.query({ projectId: testProjectId }).execute({ queryBody: errorQuery })
|
||||
|
||||
if (!result.success) {
|
||||
console.error('Failed to execute error query:', (result as any).error)
|
||||
@@ -1082,9 +1064,7 @@ describe('API Client Integration Tests', { concurrent: false }, () => {
|
||||
},
|
||||
}
|
||||
|
||||
const result = await client
|
||||
.query({ projectId: testProjectId })
|
||||
.execute({ queryBody: trendsQuery })
|
||||
const result = await client.query({ projectId: testProjectId }).execute({ queryBody: trendsQuery })
|
||||
|
||||
expect(result.success).toBe(true)
|
||||
|
||||
@@ -1126,7 +1106,7 @@ describe('API Client Integration Tests', { concurrent: false }, () => {
|
||||
description?: string
|
||||
}>
|
||||
} = {}
|
||||
) => {
|
||||
): Promise<any> => {
|
||||
const timestamp = Date.now()
|
||||
const createResult = await client.experiments({ projectId: testProjectId }).create({
|
||||
name: options.name || `Test Experiment ${timestamp}`,
|
||||
@@ -1160,9 +1140,7 @@ describe('API Client Integration Tests', { concurrent: false }, () => {
|
||||
return createResult.data
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`Failed to create test experiment: ${(createResult as any).error?.message}`
|
||||
)
|
||||
throw new Error(`Failed to create test experiment: ${(createResult as any).error?.message}`)
|
||||
}
|
||||
|
||||
it.skip('should list experiments', async () => {
|
||||
@@ -1344,19 +1322,17 @@ describe('API Client Integration Tests', { concurrent: false }, () => {
|
||||
})
|
||||
|
||||
// Try to get exposures (may not have data immediately)
|
||||
const exposureResult = await client
|
||||
.experiments({ projectId: testProjectId })
|
||||
.getExposures({
|
||||
experimentId: experiment.id,
|
||||
refresh: true,
|
||||
})
|
||||
const exposureResult = await client.experiments({ projectId: testProjectId }).getExposures({
|
||||
experimentId: experiment.id,
|
||||
refresh: true,
|
||||
})
|
||||
|
||||
// Should succeed even if no exposure data yet
|
||||
expect(exposureResult.success).toBe(true)
|
||||
|
||||
if (exposureResult.success) {
|
||||
expect(exposureResult.data).toHaveProperty('exposures')
|
||||
expect(exposureResult.data.exposures).toBeDefined()
|
||||
expect(exposureResult.data.exposures).toBeTruthy()
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1366,12 +1342,10 @@ describe('API Client Integration Tests', { concurrent: false }, () => {
|
||||
draft: true,
|
||||
})
|
||||
|
||||
const exposureResult = await client
|
||||
.experiments({ projectId: testProjectId })
|
||||
.getExposures({
|
||||
experimentId: experiment.id,
|
||||
refresh: false,
|
||||
})
|
||||
const exposureResult = await client.experiments({ projectId: testProjectId }).getExposures({
|
||||
experimentId: experiment.id,
|
||||
refresh: false,
|
||||
})
|
||||
|
||||
expect(exposureResult.success).toBe(false)
|
||||
expect((exposureResult as any).error.message).toContain('has not started yet')
|
||||
@@ -1393,12 +1367,10 @@ describe('API Client Integration Tests', { concurrent: false }, () => {
|
||||
})
|
||||
|
||||
// Try to get metric results
|
||||
const metricsResult = await client
|
||||
.experiments({ projectId: testProjectId })
|
||||
.getMetricResults({
|
||||
experimentId: experiment.id,
|
||||
refresh: true,
|
||||
})
|
||||
const metricsResult = await client.experiments({ projectId: testProjectId }).getMetricResults({
|
||||
experimentId: experiment.id,
|
||||
refresh: true,
|
||||
})
|
||||
|
||||
expect(metricsResult.success).toBe(true)
|
||||
|
||||
@@ -1417,12 +1389,10 @@ describe('API Client Integration Tests', { concurrent: false }, () => {
|
||||
draft: true,
|
||||
})
|
||||
|
||||
const metricsResult = await client
|
||||
.experiments({ projectId: testProjectId })
|
||||
.getMetricResults({
|
||||
experimentId: experiment.id,
|
||||
refresh: false,
|
||||
})
|
||||
const metricsResult = await client.experiments({ projectId: testProjectId }).getMetricResults({
|
||||
experimentId: experiment.id,
|
||||
refresh: false,
|
||||
})
|
||||
|
||||
expect(metricsResult.success).toBe(false)
|
||||
expect((metricsResult as any).error.message).toContain('has not started yet')
|
||||
@@ -1466,12 +1436,7 @@ describe('API Client Integration Tests', { concurrent: false }, () => {
|
||||
if (updateResult.success) {
|
||||
expect(updateResult.data.parameters?.feature_flag_variants).toHaveLength(4)
|
||||
const variants = updateResult.data.parameters?.feature_flag_variants || []
|
||||
expect(variants.map((v) => v.key)).toEqual([
|
||||
'control',
|
||||
'variant_a',
|
||||
'variant_b',
|
||||
'variant_c',
|
||||
])
|
||||
expect(variants.map((v) => v.key)).toEqual(['control', 'variant_a', 'variant_b', 'variant_c'])
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1563,9 +1528,7 @@ describe('API Client Integration Tests', { concurrent: false }, () => {
|
||||
createdResources.experiments.push(experimentId)
|
||||
|
||||
// READ
|
||||
const getResult = await client
|
||||
.experiments({ projectId: testProjectId })
|
||||
.get({ experimentId })
|
||||
const getResult = await client.experiments({ projectId: testProjectId }).get({ experimentId })
|
||||
|
||||
expect(getResult.success).toBe(true)
|
||||
|
||||
@@ -1592,9 +1555,7 @@ describe('API Client Integration Tests', { concurrent: false }, () => {
|
||||
}
|
||||
|
||||
// DELETE
|
||||
const deleteResult = await client
|
||||
.experiments({ projectId: testProjectId })
|
||||
.delete({ experimentId })
|
||||
const deleteResult = await client.experiments({ projectId: testProjectId }).delete({ experimentId })
|
||||
|
||||
expect(deleteResult.success).toBe(true)
|
||||
if (deleteResult.success) {
|
||||
@@ -1603,9 +1564,7 @@ describe('API Client Integration Tests', { concurrent: false }, () => {
|
||||
}
|
||||
|
||||
// Verify deletion worked
|
||||
const getAfterDeleteResult = await client
|
||||
.experiments({ projectId: testProjectId })
|
||||
.get({ experimentId })
|
||||
const getAfterDeleteResult = await client.experiments({ projectId: testProjectId }).get({ experimentId })
|
||||
|
||||
expect(getAfterDeleteResult.success).toBe(false)
|
||||
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
|
||||
import { describe, expect, it, beforeEach } from 'vitest'
|
||||
import { strFromU8, unzipSync } from 'fflate'
|
||||
import { beforeEach, describe, expect, it } from 'vitest'
|
||||
|
||||
import { registerIntegrationResources } from '@/resources/integration'
|
||||
import { ResourceUri } from '@/resources/integration/index'
|
||||
import {
|
||||
getSupportedFrameworks,
|
||||
EXAMPLES_MARKDOWN_URL,
|
||||
FRAMEWORK_MARKDOWN_FILES,
|
||||
getSupportedFrameworks,
|
||||
} from '@/resources/integration/framework-mappings'
|
||||
import { ResourceUri } from '@/resources/integration/index'
|
||||
import type { Context } from '@/tools/types'
|
||||
import { unzipSync, strFromU8 } from 'fflate'
|
||||
|
||||
const createMockContext = (): Context => ({
|
||||
api: {} as any,
|
||||
@@ -41,7 +42,7 @@ describe('Example Resources - Markdown Artifact Loading', () => {
|
||||
const uint8Array = new Uint8Array(arrayBuffer)
|
||||
const unzipped = unzipSync(uint8Array)
|
||||
|
||||
expect(unzipped).toBeDefined()
|
||||
expect(unzipped).toBeTruthy()
|
||||
expect(Object.keys(unzipped).length).toBeGreaterThan(0)
|
||||
}, 30000) // 30 second timeout for network request
|
||||
|
||||
@@ -54,10 +55,9 @@ describe('Example Resources - Markdown Artifact Loading', () => {
|
||||
const frameworks = getSupportedFrameworks()
|
||||
|
||||
for (const framework of frameworks) {
|
||||
const filename =
|
||||
FRAMEWORK_MARKDOWN_FILES[framework as keyof typeof FRAMEWORK_MARKDOWN_FILES]
|
||||
const filename = FRAMEWORK_MARKDOWN_FILES[framework as keyof typeof FRAMEWORK_MARKDOWN_FILES]
|
||||
const fileData = unzipped[filename]
|
||||
expect(fileData).toBeDefined()
|
||||
expect(fileData).toBeTruthy()
|
||||
expect(fileData!.length).toBeGreaterThan(0)
|
||||
}
|
||||
}, 30000)
|
||||
@@ -71,10 +71,9 @@ describe('Example Resources - Markdown Artifact Loading', () => {
|
||||
const frameworks = getSupportedFrameworks()
|
||||
|
||||
for (const framework of frameworks) {
|
||||
const filename =
|
||||
FRAMEWORK_MARKDOWN_FILES[framework as keyof typeof FRAMEWORK_MARKDOWN_FILES]
|
||||
const filename = FRAMEWORK_MARKDOWN_FILES[framework as keyof typeof FRAMEWORK_MARKDOWN_FILES]
|
||||
const markdownData = unzipped[filename]
|
||||
expect(markdownData).toBeDefined()
|
||||
expect(markdownData).toBeTruthy()
|
||||
|
||||
const markdown = strFromU8(markdownData!)
|
||||
expect(markdown).toBeTruthy()
|
||||
@@ -95,17 +94,15 @@ describe('Example Resources - Markdown Artifact Loading', () => {
|
||||
.startsWith(ResourceUri.EXAMPLE_PROJECT_FRAMEWORK.replace('{framework}', ''))
|
||||
) as any
|
||||
|
||||
expect(exampleTemplate).toBeDefined()
|
||||
expect(exampleTemplate).toBeTruthy()
|
||||
|
||||
const frameworks = getSupportedFrameworks()
|
||||
|
||||
for (const framework of frameworks) {
|
||||
const uri = new URL(
|
||||
ResourceUri.EXAMPLE_PROJECT_FRAMEWORK.replace('{framework}', framework)
|
||||
)
|
||||
const uri = new URL(ResourceUri.EXAMPLE_PROJECT_FRAMEWORK.replace('{framework}', framework))
|
||||
const result = await exampleTemplate.readCallback(uri, { framework })
|
||||
|
||||
expect(result).toBeDefined()
|
||||
expect(result).toBeTruthy()
|
||||
expect(result.contents).toHaveLength(1)
|
||||
expect(result.contents[0].text).toBeTruthy()
|
||||
expect(result.contents[0].text).toContain('# PostHog')
|
||||
@@ -122,13 +119,11 @@ describe('Example Resources - Markdown Artifact Loading', () => {
|
||||
) as any
|
||||
|
||||
const invalidFramework = 'invalid-framework-xyz'
|
||||
const uri = new URL(
|
||||
ResourceUri.EXAMPLE_PROJECT_FRAMEWORK.replace('{framework}', invalidFramework)
|
||||
)
|
||||
const uri = new URL(ResourceUri.EXAMPLE_PROJECT_FRAMEWORK.replace('{framework}', invalidFramework))
|
||||
|
||||
await expect(
|
||||
exampleTemplate.readCallback(uri, { framework: invalidFramework })
|
||||
).rejects.toThrow(/is not supported yet/)
|
||||
await expect(exampleTemplate.readCallback(uri, { framework: invalidFramework })).rejects.toThrow(
|
||||
/is not supported yet/
|
||||
)
|
||||
}, 30000)
|
||||
|
||||
it('should cache the markdown ZIP and reuse it', async () => {
|
||||
@@ -142,16 +137,12 @@ describe('Example Resources - Markdown Artifact Loading', () => {
|
||||
const framework = 'nextjs-app-router'
|
||||
|
||||
// First call should fetch and cache
|
||||
const uri1 = new URL(
|
||||
ResourceUri.EXAMPLE_PROJECT_FRAMEWORK.replace('{framework}', framework)
|
||||
)
|
||||
const uri1 = new URL(ResourceUri.EXAMPLE_PROJECT_FRAMEWORK.replace('{framework}', framework))
|
||||
const result1 = await exampleTemplate.readCallback(uri1, { framework })
|
||||
expect(result1.contents[0].text).toBeTruthy()
|
||||
|
||||
// Second call should use cache (we can't directly verify caching, but we verify it works)
|
||||
const uri2 = new URL(
|
||||
ResourceUri.EXAMPLE_PROJECT_FRAMEWORK.replace('{framework}', framework)
|
||||
)
|
||||
const uri2 = new URL(ResourceUri.EXAMPLE_PROJECT_FRAMEWORK.replace('{framework}', framework))
|
||||
const result2 = await exampleTemplate.readCallback(uri2, { framework })
|
||||
expect(result2.contents[0].text).toBeTruthy()
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { SessionManager } from '@/lib/utils/SessionManager'
|
||||
import { getToolsFromContext } from '@/tools'
|
||||
import type { Context } from '@/tools/types'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
const createMockContext = (): Context => ({
|
||||
api: {} as any,
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { expect } from 'vitest'
|
||||
|
||||
import { ApiClient } from '@/api/client'
|
||||
import { SessionManager } from '@/lib/utils/SessionManager'
|
||||
import { StateManager } from '@/lib/utils/StateManager'
|
||||
import { MemoryCache } from '@/lib/utils/cache/MemoryCache'
|
||||
import type { InsightQuery } from '@/schema/query'
|
||||
import type { Context } from '@/tools/types'
|
||||
import { expect } from 'vitest'
|
||||
|
||||
export const API_BASE_URL = process.env.TEST_POSTHOG_API_BASE_URL || 'http://localhost:8010'
|
||||
export const API_TOKEN = process.env.TEST_POSTHOG_PERSONAL_API_KEY
|
||||
@@ -18,7 +19,7 @@ export interface CreatedResources {
|
||||
surveys: string[]
|
||||
}
|
||||
|
||||
export function validateEnvironmentVariables() {
|
||||
export function validateEnvironmentVariables(): void {
|
||||
if (!API_TOKEN) {
|
||||
throw new Error('TEST_POSTHOG_PERSONAL_API_KEY environment variable is required')
|
||||
}
|
||||
@@ -54,7 +55,7 @@ export function createTestContext(client: ApiClient): Context {
|
||||
return context
|
||||
}
|
||||
|
||||
export async function setActiveProjectAndOrg(context: Context, projectId: string, orgId: string) {
|
||||
export async function setActiveProjectAndOrg(context: Context, projectId: string, orgId: string): Promise<void> {
|
||||
const cache = context.cache
|
||||
await cache.set('projectId', projectId)
|
||||
await cache.set('orgId', orgId)
|
||||
@@ -64,7 +65,7 @@ export async function cleanupResources(
|
||||
client: ApiClient,
|
||||
projectId: string,
|
||||
resources: CreatedResources
|
||||
) {
|
||||
): Promise<void> {
|
||||
for (const flagId of resources.featureFlags) {
|
||||
try {
|
||||
await client.featureFlags({ projectId }).delete({ flagId })
|
||||
@@ -102,8 +103,8 @@ export async function cleanupResources(
|
||||
resources.surveys = []
|
||||
}
|
||||
|
||||
export function parseToolResponse(result: any) {
|
||||
expect(result.content).toBeDefined()
|
||||
export function parseToolResponse(result: any): any {
|
||||
expect(result.content).toBeTruthy()
|
||||
expect(result.content[0].type).toBe('text')
|
||||
return JSON.parse(result.content[0].text)
|
||||
}
|
||||
@@ -143,12 +144,7 @@ export const SAMPLE_HOGQL_QUERIES: Record<SampleHogQLQuery, InsightQuery> = {
|
||||
},
|
||||
}
|
||||
|
||||
type SampleTrendQuery =
|
||||
| 'basicPageviews'
|
||||
| 'uniqueUsers'
|
||||
| 'multipleEvents'
|
||||
| 'withBreakdown'
|
||||
| 'withPropertyFilter'
|
||||
type SampleTrendQuery = 'basicPageviews' | 'uniqueUsers' | 'multipleEvents' | 'withBreakdown' | 'withPropertyFilter'
|
||||
|
||||
export const SAMPLE_TREND_QUERIES: Record<SampleTrendQuery, InsightQuery> = {
|
||||
basicPageviews: {
|
||||
|
||||
@@ -1,21 +1,22 @@
|
||||
import { describe, it, expect, beforeAll, afterEach } from 'vitest'
|
||||
import { afterEach, beforeAll, describe, expect, it } from 'vitest'
|
||||
|
||||
import {
|
||||
validateEnvironmentVariables,
|
||||
type CreatedResources,
|
||||
TEST_ORG_ID,
|
||||
TEST_PROJECT_ID,
|
||||
cleanupResources,
|
||||
createTestClient,
|
||||
createTestContext,
|
||||
setActiveProjectAndOrg,
|
||||
cleanupResources,
|
||||
parseToolResponse,
|
||||
generateUniqueKey,
|
||||
TEST_PROJECT_ID,
|
||||
TEST_ORG_ID,
|
||||
type CreatedResources,
|
||||
parseToolResponse,
|
||||
setActiveProjectAndOrg,
|
||||
validateEnvironmentVariables,
|
||||
} from '@/shared/test-utils'
|
||||
import createDashboardTool from '@/tools/dashboards/create'
|
||||
import updateDashboardTool from '@/tools/dashboards/update'
|
||||
import deleteDashboardTool from '@/tools/dashboards/delete'
|
||||
import getAllDashboardsTool from '@/tools/dashboards/getAll'
|
||||
import getDashboardTool from '@/tools/dashboards/get'
|
||||
import getAllDashboardsTool from '@/tools/dashboards/getAll'
|
||||
import updateDashboardTool from '@/tools/dashboards/update'
|
||||
import type { Context } from '@/tools/types'
|
||||
|
||||
describe('Dashboards', { concurrent: false }, () => {
|
||||
@@ -53,7 +54,7 @@ describe('Dashboards', { concurrent: false }, () => {
|
||||
const result = await createTool.handler(context, params)
|
||||
const dashboardData = parseToolResponse(result)
|
||||
|
||||
expect(dashboardData.id).toBeDefined()
|
||||
expect(dashboardData.id).toBeTruthy()
|
||||
expect(dashboardData.name).toBe(params.data.name)
|
||||
expect(dashboardData.url).toContain('/dashboard/')
|
||||
|
||||
@@ -73,7 +74,7 @@ describe('Dashboards', { concurrent: false }, () => {
|
||||
const result = await createTool.handler(context, params)
|
||||
const dashboardData = parseToolResponse(result)
|
||||
|
||||
expect(dashboardData.id).toBeDefined()
|
||||
expect(dashboardData.id).toBeTruthy()
|
||||
expect(dashboardData.name).toBe(params.data.name)
|
||||
|
||||
createdResources.dashboards.push(dashboardData.id)
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import { describe, it, expect, beforeAll, afterEach } from 'vitest'
|
||||
import { afterEach, beforeAll, describe, expect, it } from 'vitest'
|
||||
|
||||
import {
|
||||
validateEnvironmentVariables,
|
||||
type CreatedResources,
|
||||
TEST_ORG_ID,
|
||||
TEST_PROJECT_ID,
|
||||
cleanupResources,
|
||||
createTestClient,
|
||||
createTestContext,
|
||||
setActiveProjectAndOrg,
|
||||
cleanupResources,
|
||||
TEST_PROJECT_ID,
|
||||
TEST_ORG_ID,
|
||||
type CreatedResources,
|
||||
validateEnvironmentVariables,
|
||||
} from '@/shared/test-utils'
|
||||
import searchDocsTool from '@/tools/documentation/searchDocs'
|
||||
import type { Context } from '@/tools/types'
|
||||
@@ -54,7 +55,7 @@ describe('Documentation', { concurrent: false }, () => {
|
||||
})
|
||||
|
||||
expect(result.content[0].type).toBe('text')
|
||||
expect(result.content[0].text).toBeDefined()
|
||||
expect(result.content[0].text).toBeTruthy()
|
||||
expect(result.content[0].text.length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
@@ -64,7 +65,7 @@ describe('Documentation', { concurrent: false }, () => {
|
||||
})
|
||||
|
||||
expect(result.content[0].type).toBe('text')
|
||||
expect(result.content[0].text).toBeDefined()
|
||||
expect(result.content[0].text).toBeTruthy()
|
||||
expect(result.content[0].text.length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
@@ -74,7 +75,7 @@ describe('Documentation', { concurrent: false }, () => {
|
||||
})
|
||||
|
||||
expect(result.content[0].type).toBe('text')
|
||||
expect(result.content[0].text).toBeDefined()
|
||||
expect(result.content[0].text).toBeTruthy()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -86,7 +87,7 @@ describe('Documentation', { concurrent: false }, () => {
|
||||
await searchTool.handler(context, { query: '' })
|
||||
expect.fail('Should have thrown validation error')
|
||||
} catch (error) {
|
||||
expect(error).toBeDefined()
|
||||
expect(error).toBeTruthy()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,19 +1,20 @@
|
||||
import { describe, it, expect, beforeAll, afterEach } from 'vitest'
|
||||
import { afterEach, beforeAll, describe, expect, it } from 'vitest'
|
||||
|
||||
import { OrderByErrors, OrderDirectionErrors, StatusErrors } from '@/schema/errors'
|
||||
import {
|
||||
validateEnvironmentVariables,
|
||||
type CreatedResources,
|
||||
TEST_ORG_ID,
|
||||
TEST_PROJECT_ID,
|
||||
cleanupResources,
|
||||
createTestClient,
|
||||
createTestContext,
|
||||
setActiveProjectAndOrg,
|
||||
cleanupResources,
|
||||
TEST_PROJECT_ID,
|
||||
TEST_ORG_ID,
|
||||
type CreatedResources,
|
||||
parseToolResponse,
|
||||
setActiveProjectAndOrg,
|
||||
validateEnvironmentVariables,
|
||||
} from '@/shared/test-utils'
|
||||
import listErrorsTool from '@/tools/errorTracking/listErrors'
|
||||
import errorDetailsTool from '@/tools/errorTracking/errorDetails'
|
||||
import listErrorsTool from '@/tools/errorTracking/listErrors'
|
||||
import type { Context } from '@/tools/types'
|
||||
import { OrderByErrors, OrderDirectionErrors, StatusErrors } from '@/schema/errors'
|
||||
|
||||
describe('Error Tracking', { concurrent: false }, () => {
|
||||
let context: Context
|
||||
|
||||
@@ -1,20 +1,21 @@
|
||||
import { describe, it, expect, beforeAll, afterEach } from 'vitest'
|
||||
import { afterEach, beforeAll, describe, expect, it } from 'vitest'
|
||||
|
||||
import {
|
||||
validateEnvironmentVariables,
|
||||
type CreatedResources,
|
||||
TEST_ORG_ID,
|
||||
TEST_PROJECT_ID,
|
||||
cleanupResources,
|
||||
createTestClient,
|
||||
createTestContext,
|
||||
setActiveProjectAndOrg,
|
||||
cleanupResources,
|
||||
TEST_PROJECT_ID,
|
||||
TEST_ORG_ID,
|
||||
type CreatedResources,
|
||||
parseToolResponse,
|
||||
generateUniqueKey,
|
||||
parseToolResponse,
|
||||
setActiveProjectAndOrg,
|
||||
validateEnvironmentVariables,
|
||||
} from '@/shared/test-utils'
|
||||
import createExperimentTool from '@/tools/experiments/create'
|
||||
import deleteExperimentTool from '@/tools/experiments/delete'
|
||||
import getAllExperimentsTool from '@/tools/experiments/getAll'
|
||||
import getExperimentTool from '@/tools/experiments/get'
|
||||
import getAllExperimentsTool from '@/tools/experiments/getAll'
|
||||
import getExperimentResultsTool from '@/tools/experiments/getResults'
|
||||
import updateExperimentTool from '@/tools/experiments/update'
|
||||
import type { Context } from '@/tools/types'
|
||||
@@ -30,7 +31,7 @@ describe('Experiments', { concurrent: false }, () => {
|
||||
const createdExperiments: number[] = []
|
||||
|
||||
// Helper function to track created experiments and their feature flags
|
||||
const trackExperiment = (experiment: any) => {
|
||||
const trackExperiment = (experiment: any): void => {
|
||||
if (experiment.id) {
|
||||
createdExperiments.push(experiment.id)
|
||||
}
|
||||
@@ -80,7 +81,7 @@ describe('Experiments', { concurrent: false }, () => {
|
||||
const result = await createTool.handler(context, params as any)
|
||||
const experiment = parseToolResponse(result)
|
||||
|
||||
expect(experiment.id).toBeDefined()
|
||||
expect(experiment.id).toBeTruthy()
|
||||
expect(experiment.name).toBe(params.name)
|
||||
expect(experiment.feature_flag_key).toBe(params.feature_flag_key)
|
||||
expect(experiment.start_date).toBeNull() // Draft experiments have no start date
|
||||
@@ -103,7 +104,7 @@ describe('Experiments', { concurrent: false }, () => {
|
||||
const result = await createTool.handler(context, params as any)
|
||||
const experiment = parseToolResponse(result)
|
||||
|
||||
expect(experiment.id).toBeDefined()
|
||||
expect(experiment.id).toBeTruthy()
|
||||
expect(experiment.name).toBe(params.name)
|
||||
expect(experiment.feature_flag_key).toBe(params.feature_flag_key)
|
||||
|
||||
@@ -127,7 +128,7 @@ describe('Experiments', { concurrent: false }, () => {
|
||||
const result = await createTool.handler(context, params as any)
|
||||
const experiment = parseToolResponse(result)
|
||||
|
||||
expect(experiment.id).toBeDefined()
|
||||
expect(experiment.id).toBeTruthy()
|
||||
expect(experiment.parameters?.feature_flag_variants).toHaveLength(3)
|
||||
expect(experiment.parameters?.feature_flag_variants?.[0]?.key).toBe('control')
|
||||
expect(experiment.parameters?.feature_flag_variants?.[0]?.rollout_percentage).toBe(33)
|
||||
@@ -156,7 +157,7 @@ describe('Experiments', { concurrent: false }, () => {
|
||||
const result = await createTool.handler(context, params as any)
|
||||
const experiment = parseToolResponse(result)
|
||||
|
||||
expect(experiment.id).toBeDefined()
|
||||
expect(experiment.id).toBeTruthy()
|
||||
expect(experiment.metrics).toHaveLength(1)
|
||||
|
||||
trackExperiment(experiment)
|
||||
@@ -183,7 +184,7 @@ describe('Experiments', { concurrent: false }, () => {
|
||||
const result = await createTool.handler(context, params as any)
|
||||
const experiment = parseToolResponse(result)
|
||||
|
||||
expect(experiment.id).toBeDefined()
|
||||
expect(experiment.id).toBeTruthy()
|
||||
expect(experiment.metrics).toHaveLength(1)
|
||||
|
||||
trackExperiment(experiment)
|
||||
@@ -209,7 +210,7 @@ describe('Experiments', { concurrent: false }, () => {
|
||||
const result = await createTool.handler(context, params as any)
|
||||
const experiment = parseToolResponse(result)
|
||||
|
||||
expect(experiment.id).toBeDefined()
|
||||
expect(experiment.id).toBeTruthy()
|
||||
expect(experiment.metrics).toHaveLength(1)
|
||||
|
||||
trackExperiment(experiment)
|
||||
@@ -252,7 +253,7 @@ describe('Experiments', { concurrent: false }, () => {
|
||||
const result = await createTool.handler(context, params as any)
|
||||
const experiment = parseToolResponse(result)
|
||||
|
||||
expect(experiment.id).toBeDefined()
|
||||
expect(experiment.id).toBeTruthy()
|
||||
expect(experiment.metrics).toHaveLength(2)
|
||||
expect(experiment.metrics_secondary).toHaveLength(2)
|
||||
|
||||
@@ -272,7 +273,7 @@ describe('Experiments', { concurrent: false }, () => {
|
||||
const result = await createTool.handler(context, params as any)
|
||||
const experiment = parseToolResponse(result)
|
||||
|
||||
expect(experiment.id).toBeDefined()
|
||||
expect(experiment.id).toBeTruthy()
|
||||
|
||||
trackExperiment(experiment)
|
||||
})
|
||||
@@ -290,7 +291,7 @@ describe('Experiments', { concurrent: false }, () => {
|
||||
const result = await createTool.handler(context, params as any)
|
||||
const experiment = parseToolResponse(result)
|
||||
|
||||
expect(experiment.id).toBeDefined()
|
||||
expect(experiment.id).toBeTruthy()
|
||||
|
||||
trackExperiment(experiment)
|
||||
})
|
||||
@@ -306,7 +307,7 @@ describe('Experiments', { concurrent: false }, () => {
|
||||
const result = await createTool.handler(context, params as any)
|
||||
const experiment = parseToolResponse(result)
|
||||
|
||||
expect(experiment.id).toBeDefined()
|
||||
expect(experiment.id).toBeTruthy()
|
||||
trackExperiment(experiment)
|
||||
})
|
||||
})
|
||||
@@ -343,7 +344,7 @@ describe('Experiments', { concurrent: false }, () => {
|
||||
// Verify our test experiments are in the list
|
||||
for (const testExp of testExperiments) {
|
||||
const found = allExperiments.find((e: any) => e.id === testExp.id)
|
||||
expect(found).toBeDefined()
|
||||
expect(found).toBeTruthy()
|
||||
}
|
||||
})
|
||||
|
||||
@@ -391,9 +392,7 @@ describe('Experiments', { concurrent: false }, () => {
|
||||
it('should handle non-existent experiment ID', async () => {
|
||||
const nonExistentId = 999999
|
||||
|
||||
await expect(
|
||||
getTool.handler(context, { experimentId: nonExistentId })
|
||||
).rejects.toThrow()
|
||||
await expect(getTool.handler(context, { experimentId: nonExistentId })).rejects.toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -521,7 +520,7 @@ describe('Experiments', { concurrent: false }, () => {
|
||||
trackExperiment(createdExperiment)
|
||||
|
||||
// Verify creation
|
||||
expect(createdExperiment.id).toBeDefined()
|
||||
expect(createdExperiment.id).toBeTruthy()
|
||||
expect(createdExperiment.name).toBe(createParams.name)
|
||||
expect(createdExperiment.parameters?.feature_flag_variants).toHaveLength(2)
|
||||
expect(createdExperiment.metrics).toHaveLength(2)
|
||||
@@ -538,7 +537,7 @@ describe('Experiments', { concurrent: false }, () => {
|
||||
const listResult = await getAllTool.handler(context, {})
|
||||
const allExperiments = parseToolResponse(listResult)
|
||||
const found = allExperiments.find((e: any) => e.id === createdExperiment.id)
|
||||
expect(found).toBeDefined()
|
||||
expect(found).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should create experiment with complex funnel metrics', async () => {
|
||||
@@ -579,7 +578,7 @@ describe('Experiments', { concurrent: false }, () => {
|
||||
const result = await createTool.handler(context, params as any)
|
||||
const experiment = parseToolResponse(result)
|
||||
|
||||
expect(experiment.id).toBeDefined()
|
||||
expect(experiment.id).toBeTruthy()
|
||||
expect(experiment.metrics).toHaveLength(1)
|
||||
expect(experiment.metrics_secondary).toHaveLength(1)
|
||||
|
||||
@@ -603,7 +602,7 @@ describe('Experiments', { concurrent: false }, () => {
|
||||
const result = await createTool.handler(context, params as any)
|
||||
const experiment = parseToolResponse(result)
|
||||
|
||||
expect(experiment.id).toBeDefined()
|
||||
expect(experiment.id).toBeTruthy()
|
||||
|
||||
trackExperiment(experiment)
|
||||
})
|
||||
@@ -621,7 +620,7 @@ describe('Experiments', { concurrent: false }, () => {
|
||||
const result = await createTool.handler(context, params as any)
|
||||
const experiment = parseToolResponse(result)
|
||||
|
||||
expect(experiment.id).toBeDefined()
|
||||
expect(experiment.id).toBeTruthy()
|
||||
|
||||
trackExperiment(experiment)
|
||||
})
|
||||
@@ -644,7 +643,7 @@ describe('Experiments', { concurrent: false }, () => {
|
||||
const result = await createTool.handler(context, params as any)
|
||||
const experiment = parseToolResponse(result)
|
||||
|
||||
expect(experiment.id).toBeDefined()
|
||||
expect(experiment.id).toBeTruthy()
|
||||
expect(experiment.metrics || []).toHaveLength(0)
|
||||
expect(experiment.metrics_secondary || []).toHaveLength(0)
|
||||
|
||||
@@ -687,7 +686,7 @@ describe('Experiments', { concurrent: false }, () => {
|
||||
trackExperiment(experiment)
|
||||
} catch (error) {
|
||||
// Expected for invalid configuration
|
||||
expect(error).toBeDefined()
|
||||
expect(error).toBeTruthy()
|
||||
}
|
||||
})
|
||||
|
||||
@@ -710,7 +709,7 @@ describe('Experiments', { concurrent: false }, () => {
|
||||
const result = await createTool.handler(context, params as any)
|
||||
const experiment = parseToolResponse(result)
|
||||
|
||||
expect(experiment.id).toBeDefined()
|
||||
expect(experiment.id).toBeTruthy()
|
||||
expect(experiment.metrics).toHaveLength(1)
|
||||
|
||||
trackExperiment(experiment)
|
||||
@@ -736,7 +735,7 @@ describe('Experiments', { concurrent: false }, () => {
|
||||
const result = await createTool.handler(context, params as any)
|
||||
const experiment = parseToolResponse(result)
|
||||
|
||||
expect(experiment.id).toBeDefined()
|
||||
expect(experiment.id).toBeTruthy()
|
||||
|
||||
trackExperiment(experiment)
|
||||
})
|
||||
@@ -754,11 +753,11 @@ describe('Experiments', { concurrent: false }, () => {
|
||||
try {
|
||||
const result = await createTool.handler(context, params as any)
|
||||
const experiment = parseToolResponse(result)
|
||||
expect(experiment.id).toBeDefined()
|
||||
expect(experiment.id).toBeTruthy()
|
||||
trackExperiment(experiment)
|
||||
} catch (error) {
|
||||
// Some APIs might reject very long names
|
||||
expect(error).toBeDefined()
|
||||
expect(error).toBeTruthy()
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -779,7 +778,7 @@ describe('Experiments', { concurrent: false }, () => {
|
||||
|
||||
const createResult = await createTool.handler(context, createParams as any)
|
||||
const experiment = parseToolResponse(createResult)
|
||||
expect(experiment.id).toBeDefined()
|
||||
expect(experiment.id).toBeTruthy()
|
||||
trackExperiment(experiment)
|
||||
|
||||
// Delete the experiment
|
||||
@@ -811,7 +810,7 @@ describe('Experiments', { concurrent: false }, () => {
|
||||
await deleteTool.handler(context, deleteParams)
|
||||
expect.fail('Should have thrown an error for invalid experiment ID')
|
||||
} catch (error) {
|
||||
expect(error).toBeDefined()
|
||||
expect(error).toBeTruthy()
|
||||
expect((error as Error).message).toContain('Failed to delete experiment')
|
||||
}
|
||||
})
|
||||
@@ -828,7 +827,7 @@ describe('Experiments', { concurrent: false }, () => {
|
||||
|
||||
const createResult = await createTool.handler(context, createParams as any)
|
||||
const experiment = parseToolResponse(createResult)
|
||||
expect(experiment.id).toBeDefined()
|
||||
expect(experiment.id).toBeTruthy()
|
||||
trackExperiment(experiment)
|
||||
|
||||
// Delete the experiment twice
|
||||
@@ -844,7 +843,7 @@ describe('Experiments', { concurrent: false }, () => {
|
||||
await deleteTool.handler(context, deleteParams)
|
||||
expect.fail('Should have thrown an error for already deleted experiment')
|
||||
} catch (error) {
|
||||
expect(error).toBeDefined()
|
||||
expect(error).toBeTruthy()
|
||||
expect((error as Error).message).toContain('Failed to delete experiment')
|
||||
expect((error as Error).message).toContain('404')
|
||||
}
|
||||
@@ -866,7 +865,7 @@ describe('Experiments', { concurrent: false }, () => {
|
||||
await deleteTool.handler(context, {} as any)
|
||||
expect.fail('Should have thrown validation error for missing experimentId')
|
||||
} catch (error) {
|
||||
expect(error).toBeDefined()
|
||||
expect(error).toBeTruthy()
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -888,7 +887,7 @@ describe('Experiments', { concurrent: false }, () => {
|
||||
|
||||
const createResult = await createTool.handler(context, createParams as any)
|
||||
const experiment = parseToolResponse(createResult)
|
||||
expect(experiment.id).toBeDefined()
|
||||
expect(experiment.id).toBeTruthy()
|
||||
trackExperiment(experiment)
|
||||
|
||||
// Update basic fields
|
||||
@@ -937,7 +936,7 @@ describe('Experiments', { concurrent: false }, () => {
|
||||
const updateResult = await updateTool.handler(context, launchParams)
|
||||
const launchedExperiment = parseToolResponse(updateResult)
|
||||
|
||||
expect(launchedExperiment.start_date).toBeDefined() // Running experiments have start date
|
||||
expect(launchedExperiment.start_date).toBeTruthy() // Running experiments have start date
|
||||
expect(launchedExperiment.end_date).toBeNull() // But no end date yet
|
||||
|
||||
trackExperiment(experiment)
|
||||
@@ -970,7 +969,7 @@ describe('Experiments', { concurrent: false }, () => {
|
||||
const updateResult = await updateTool.handler(context, stopParams)
|
||||
const stoppedExperiment = parseToolResponse(updateResult)
|
||||
|
||||
expect(stoppedExperiment.end_date).toBeDefined()
|
||||
expect(stoppedExperiment.end_date).toBeTruthy()
|
||||
// Note: API may not set conclusion field automatically, it depends on the backend implementation
|
||||
// The important thing is that end_date is set, indicating the experiment is stopped
|
||||
|
||||
@@ -1018,7 +1017,7 @@ describe('Experiments', { concurrent: false }, () => {
|
||||
expect(restartedExperiment.end_date).toBeNull()
|
||||
expect(restartedExperiment.conclusion).toBeNull()
|
||||
expect(restartedExperiment.conclusion_comment).toBeNull()
|
||||
expect(restartedExperiment.start_date).toBeDefined() // Restarted experiments have start date
|
||||
expect(restartedExperiment.start_date).toBeTruthy() // Restarted experiments have start date
|
||||
expect(restartedExperiment.end_date).toBeNull() // But no end date
|
||||
|
||||
trackExperiment(experiment)
|
||||
@@ -1157,7 +1156,7 @@ describe('Experiments', { concurrent: false }, () => {
|
||||
await updateTool.handler(context, updateParams)
|
||||
expect.fail('Should have thrown an error for invalid experiment ID')
|
||||
} catch (error) {
|
||||
expect(error).toBeDefined()
|
||||
expect(error).toBeTruthy()
|
||||
expect((error as Error).message).toContain('Failed to update experiment')
|
||||
}
|
||||
})
|
||||
@@ -1167,7 +1166,7 @@ describe('Experiments', { concurrent: false }, () => {
|
||||
await updateTool.handler(context, { data: { name: 'Test' } } as any)
|
||||
expect.fail('Should have thrown validation error for missing experimentId')
|
||||
} catch (error) {
|
||||
expect(error).toBeDefined()
|
||||
expect(error).toBeTruthy()
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1239,12 +1238,12 @@ describe('Experiments', { concurrent: false }, () => {
|
||||
const experiment = parseToolResponse(result)
|
||||
|
||||
// Check actual date fields instead of computed status
|
||||
expect(experiment.start_date).toBeDefined() // Should have start date if launched
|
||||
expect(experiment.start_date).toBeTruthy() // Should have start date if launched
|
||||
|
||||
trackExperiment(experiment)
|
||||
} catch (error) {
|
||||
// Some environments might not allow immediate launch
|
||||
expect(error).toBeDefined()
|
||||
expect(error).toBeTruthy()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,21 +1,22 @@
|
||||
import { describe, it, expect, beforeAll, afterEach } from 'vitest'
|
||||
import { afterEach, beforeAll, describe, expect, it } from 'vitest'
|
||||
|
||||
import {
|
||||
validateEnvironmentVariables,
|
||||
type CreatedResources,
|
||||
TEST_ORG_ID,
|
||||
TEST_PROJECT_ID,
|
||||
cleanupResources,
|
||||
createTestClient,
|
||||
createTestContext,
|
||||
setActiveProjectAndOrg,
|
||||
cleanupResources,
|
||||
TEST_PROJECT_ID,
|
||||
TEST_ORG_ID,
|
||||
type CreatedResources,
|
||||
parseToolResponse,
|
||||
generateUniqueKey,
|
||||
parseToolResponse,
|
||||
setActiveProjectAndOrg,
|
||||
validateEnvironmentVariables,
|
||||
} from '@/shared/test-utils'
|
||||
import createFeatureFlagTool from '@/tools/featureFlags/create'
|
||||
import updateFeatureFlagTool from '@/tools/featureFlags/update'
|
||||
import deleteFeatureFlagTool from '@/tools/featureFlags/delete'
|
||||
import getAllFeatureFlagsTool from '@/tools/featureFlags/getAll'
|
||||
import getFeatureFlagDefinitionTool from '@/tools/featureFlags/getDefinition'
|
||||
import updateFeatureFlagTool from '@/tools/featureFlags/update'
|
||||
import type { Context } from '@/tools/types'
|
||||
|
||||
describe('Feature Flags', { concurrent: false }, () => {
|
||||
@@ -53,7 +54,7 @@ describe('Feature Flags', { concurrent: false }, () => {
|
||||
const result = await createTool.handler(context, params)
|
||||
|
||||
const flagData = parseToolResponse(result)
|
||||
expect(flagData.id).toBeDefined()
|
||||
expect(flagData.id).toBeTruthy()
|
||||
expect(flagData.key).toBe(params.key)
|
||||
expect(flagData.name).toBe(params.name)
|
||||
expect(flagData.active).toBe(params.active)
|
||||
@@ -75,7 +76,7 @@ describe('Feature Flags', { concurrent: false }, () => {
|
||||
const result = await createTool.handler(context, params)
|
||||
const flagData = parseToolResponse(result)
|
||||
|
||||
expect(flagData.id).toBeDefined()
|
||||
expect(flagData.id).toBeTruthy()
|
||||
expect(flagData.key).toBe(params.key)
|
||||
expect(flagData.name).toBe(params.name)
|
||||
|
||||
@@ -109,7 +110,7 @@ describe('Feature Flags', { concurrent: false }, () => {
|
||||
const result = await createTool.handler(context, params)
|
||||
const flagData = parseToolResponse(result)
|
||||
|
||||
expect(flagData.id).toBeDefined()
|
||||
expect(flagData.id).toBeTruthy()
|
||||
expect(flagData.key).toBe(params.key)
|
||||
expect(flagData.name).toBe(params.name)
|
||||
|
||||
@@ -186,7 +187,7 @@ describe('Feature Flags', { concurrent: false }, () => {
|
||||
const updateResult = await updateTool.handler(context, updateParams)
|
||||
const updatedFlag = parseToolResponse(updateResult)
|
||||
|
||||
expect(updatedFlag.id).toBeDefined()
|
||||
expect(updatedFlag.id).toBeTruthy()
|
||||
expect(updatedFlag.key).toBe(createParams.key)
|
||||
})
|
||||
})
|
||||
@@ -223,7 +224,7 @@ describe('Feature Flags', { concurrent: false }, () => {
|
||||
// Verify our test flags are in the list
|
||||
for (const testFlag of testFlags) {
|
||||
const found = allFlags.find((f: any) => f.id === testFlag.id)
|
||||
expect(found).toBeDefined()
|
||||
expect(found).toBeTruthy()
|
||||
expect(found.key).toBe(testFlag.key)
|
||||
}
|
||||
})
|
||||
@@ -276,9 +277,7 @@ describe('Feature Flags', { concurrent: false }, () => {
|
||||
|
||||
const result = await getDefinitionTool.handler(context, { flagKey: nonExistentKey })
|
||||
|
||||
expect(result.content[0].text).toBe(
|
||||
`Error: Flag with key "${nonExistentKey}" not found.`
|
||||
)
|
||||
expect(result.content[0].text).toBe(`Error: Flag with key "${nonExistentKey}" not found.`)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -301,7 +300,7 @@ describe('Feature Flags', { concurrent: false }, () => {
|
||||
// Delete the flag
|
||||
const deleteResult = await deleteTool.handler(context, { flagKey: createParams.key })
|
||||
|
||||
expect(deleteResult.content).toBeDefined()
|
||||
expect(deleteResult.content).toBeTruthy()
|
||||
expect(deleteResult.content[0].type).toBe('text')
|
||||
const deleteResponse = parseToolResponse(deleteResult)
|
||||
expect(deleteResponse.success).toBe(true)
|
||||
@@ -312,9 +311,7 @@ describe('Feature Flags', { concurrent: false }, () => {
|
||||
const getResult = await getDefinitionTool.handler(context, {
|
||||
flagKey: createParams.key,
|
||||
})
|
||||
expect(getResult.content[0].text).toBe(
|
||||
`Error: Flag with key "${createParams.key}" not found.`
|
||||
)
|
||||
expect(getResult.content[0].text).toBe(`Error: Flag with key "${createParams.key}" not found.`)
|
||||
})
|
||||
|
||||
it('should handle deletion of non-existent flag', async () => {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { afterEach, beforeAll, describe, expect, it } from 'vitest'
|
||||
|
||||
import {
|
||||
type CreatedResources,
|
||||
SAMPLE_HOGQL_QUERIES,
|
||||
@@ -18,7 +20,6 @@ import getAllInsightsTool from '@/tools/insights/getAll'
|
||||
import queryInsightTool from '@/tools/insights/query'
|
||||
import updateInsightTool from '@/tools/insights/update'
|
||||
import type { Context } from '@/tools/types'
|
||||
import { afterEach, beforeAll, describe, expect, it } from 'vitest'
|
||||
|
||||
describe('Insights', { concurrent: false }, () => {
|
||||
// All tests run sequentially to avoid conflicts with shared PostHog project
|
||||
@@ -57,7 +58,7 @@ describe('Insights', { concurrent: false }, () => {
|
||||
const result = await createTool.handler(context, params)
|
||||
|
||||
const insightData = parseToolResponse(result)
|
||||
expect(insightData.id).toBeDefined()
|
||||
expect(insightData.id).toBeTruthy()
|
||||
expect(insightData.name).toBe(params.data.name)
|
||||
expect(insightData.url).toContain('/insights/')
|
||||
|
||||
@@ -77,7 +78,7 @@ describe('Insights', { concurrent: false }, () => {
|
||||
const result = await createTool.handler(context, params)
|
||||
|
||||
const insightData = parseToolResponse(result)
|
||||
expect(insightData.id).toBeDefined()
|
||||
expect(insightData.id).toBeTruthy()
|
||||
expect(insightData.name).toBe(params.data.name)
|
||||
|
||||
createdResources.insights.push(insightData.id)
|
||||
@@ -97,7 +98,7 @@ describe('Insights', { concurrent: false }, () => {
|
||||
const result = await createTool.handler(context, params)
|
||||
|
||||
const insightData = parseToolResponse(result)
|
||||
expect(insightData.id).toBeDefined()
|
||||
expect(insightData.id).toBeTruthy()
|
||||
expect(insightData.name).toBe(params.data.name)
|
||||
|
||||
createdResources.insights.push(insightData.id)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user