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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions node/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"@langchain/core": "^0.3.66",
"@langchain/openai": "^0.3.14",
"@mastra/core": "^0.24.1",
"@mastra/otel-exporter": "^0.3.3",
"@posthog/ai": "^6.6.0",
"ai": "^5.0.42",
"dotenv": "^16.4.5",
Expand All @@ -40,6 +41,7 @@
"devDependencies": {
"@types/node": "^20.0.0",
"ts-node": "^10.9.0",
"tsx": "^4.20.6",
"typescript": "^5.0.0"
}
}
74 changes: 74 additions & 0 deletions node/scripts/otel/run_mastra_test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
#!/bin/bash
# Run Mastra OTEL test

set -e # Exit on any error

# Colors for output
GREEN='\033[0;32m'
BLUE='\033[0;34m'
YELLOW='\033[1;33m'
RED='\033[0;31m'
NC='\033[0m' # No Color

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
NODE_DIR="$SCRIPT_DIR/../.."
cd "$NODE_DIR"

echo ""
echo "================================================================================"
echo "Running Mastra OTEL Test"
echo "================================================================================"
echo ""

# Load environment variables from parent .env file
if [ -f "../.env" ]; then
echo -e "${BLUE}📋 Loading environment variables from .env...${NC}"
set -a
source ../.env
set +a
else
echo -e "${RED}❌ .env file not found!${NC}"
echo " Create one with:"
echo " OPENAI_API_KEY=sk-..."
echo " POSTHOG_API_KEY=phc_test"
echo " POSTHOG_HOST=http://localhost:8000"
echo " POSTHOG_PROJECT_ID=1"
exit 1
fi

echo -e "${GREEN}✅ Environment loaded from .env${NC}"
echo ""

# Check if OpenAI API key is set
if [ -z "$OPENAI_API_KEY" ]; then
echo -e "${RED}❌ OPENAI_API_KEY not set in .env${NC}"
exit 1
fi

# Build TypeScript if needed
if [ ! -d "dist" ] || [ "scripts/otel/test_mastra_otel.ts" -nt "dist" ]; then
echo -e "${YELLOW}🔨 Building TypeScript...${NC}"
npm run build
echo -e "${GREEN}✅ Build complete${NC}"
echo ""
fi

# Run the test
echo -e "${BLUE}🚀 Running Mastra OTEL test...${NC}"
echo ""

npx tsx scripts/otel/test_mastra_otel.ts

echo ""
echo "================================================================================"
echo "Test complete!"
echo "================================================================================"
echo ""
echo -e "${BLUE}🔍 Check PostHog for traces:${NC}"
echo " ${POSTHOG_HOST}/project/${POSTHOG_PROJECT_ID}/llm-analytics/traces"
echo ""
echo "Expected:"
echo " - Service: mastra-otel-test"
echo " - User ID: mastra-test-user"
echo " - 4 conversation turns with tool calls"
echo ""
184 changes: 184 additions & 0 deletions node/scripts/otel/test_mastra_otel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
#!/usr/bin/env ts-node
/**
* Test: Mastra OTEL Integration - Multi-turn conversation with tools
*
* Tests Mastra's OpenTelemetry instrumentation with PostHog ingestion.
* Similar to the Python OTEL v2 tests but using Mastra framework.
*/

import * as dotenv from 'dotenv';
import * as path from 'path';
import { fileURLToPath } from 'url';
import { Mastra, createTool } from '@mastra/core';
import { Agent } from '@mastra/core/agent';
import { OtelExporter } from '@mastra/otel-exporter';
import { z } from 'zod';
import { trace } from '@opentelemetry/api';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

// Load environment variables
dotenv.config({ path: path.join(__dirname, '..', '..', '..', '.env') });

const OPENAI_API_KEY = process.env.OPENAI_API_KEY;
const POSTHOG_API_KEY = process.env.POSTHOG_API_KEY || 'phc_test';
const POSTHOG_PROJECT_ID = process.env.POSTHOG_PROJECT_ID || '1';
const POSTHOG_HOST = process.env.POSTHOG_HOST || 'http://localhost:8000';

console.log('\n' + '='.repeat(80));
console.log('TEST: Mastra OTEL - Multi-turn Conversation with Tools');
console.log('='.repeat(80));

if (!OPENAI_API_KEY) {
console.log('❌ OPENAI_API_KEY not set');
process.exit(1);
}

// Configure Mastra with OtelExporter pointing to PostHog OTEL endpoint
const tracesEndpoint = `${POSTHOG_HOST}/api/projects/${POSTHOG_PROJECT_ID}/ai/otel/traces`;

const mastra = new Mastra({
observability: {
configs: {
default: {
serviceName: 'mastra-otel-test',
exporters: [
new OtelExporter({
provider: {
custom: {
endpoint: tracesEndpoint,
protocol: 'http/protobuf',
headers: {
'Authorization': `Bearer ${POSTHOG_API_KEY}`,
},
},
},
timeout: 60000,
logLevel: 'info',
resourceAttributes: {
'service.name': 'mastra-otel-test',
'user.id': 'mastra-test-user',
},
}),
],
},
},
},
});

// Define tools
const getWeatherTool = createTool({
name: 'get_weather',
description: 'Get weather for a location',
inputSchema: z.object({
location: z.string().describe('City name'),
}),
execute: async ({ context }) => {
console.log(`[Tool] Getting weather for ${context.location}`);
return { weather: 'Sunny', temperature: '18°C' };
},
});

const tellJokeTool = createTool({
name: 'tell_joke',
description: 'Tell a joke',
inputSchema: z.object({
topic: z.string().describe('Joke topic'),
}),
execute: async ({ context }) => {
console.log(`[Tool] Telling joke about ${context.topic}`);
return { joke: 'Why do programmers prefer dark mode? Because light attracts bugs!' };
},
});

// Create agent with tools
const agent = new Agent({
name: 'test-agent',
instructions: 'You are a helpful assistant with access to weather and jokes.',
model: 'openai/gpt-4o-mini',
tools: {
get_weather: getWeatherTool,
tell_joke: tellJokeTool,
},
});

async function runConversation() {
console.log('\n🗣️ CONVERSATION:');
console.log('-'.repeat(80));

try {
// Turn 1: Greeting
console.log('\n[1] User: Hi there!');
let result = await agent.generate('Hi there!', {
maxSteps: 5,
});
console.log(`[1] Assistant: ${result.text}`);

// Turn 2: Weather tool call
console.log('\n[2] User: What\'s the weather in Paris?');
result = await agent.generate('What\'s the weather in Paris?', {
maxSteps: 5,
});
console.log(`[2] Assistant: ${result.text}`);

// Turn 3: Joke tool call
console.log('\n[3] User: Tell me a joke about coding');
result = await agent.generate('Tell me a joke about coding', {
maxSteps: 5,
});
console.log(`[3] Assistant: ${result.text}`);

// Turn 4: Goodbye
console.log('\n[4] User: Thanks, bye!');
result = await agent.generate('Thanks, bye!', {
maxSteps: 5,
});
console.log(`[4] Assistant: ${result.text}`);

console.log('\n' + '-'.repeat(80));
console.log('✅ Conversation complete!');
console.log(' Method: Mastra OTEL');
console.log(' Service: mastra-otel-test');
console.log('\n🔍 CHECK: PostHog should receive OTEL traces with tool calls');
console.log(' Note: Each agent.generate() call creates a separate trace (expected behavior)');

} catch (error) {
console.error('\n❌ Error:', error);
if (error instanceof Error) {
console.error(error.stack);
}
process.exit(1);
}
}

// Run the test
runConversation()
.then(async () => {
console.log('\n' + '='.repeat(80));

// Flush OTEL spans before exit
console.log('⏳ Flushing OTEL spans...');
try {
const provider = trace.getTracerProvider();
if (provider && 'forceFlush' in provider && typeof provider.forceFlush === 'function') {
await provider.forceFlush();
console.log('✅ OTEL spans flushed');
} else {
// Fallback: wait 5 seconds for auto-flush
console.log('⏳ Waiting for auto-flush (5s)...');
await new Promise(resolve => setTimeout(resolve, 5000));
}
} catch (error) {
console.error('⚠️ Error flushing OTEL spans:', error);
// Wait a bit anyway to give SDK time to flush
await new Promise(resolve => setTimeout(resolve, 5000));
}

console.log('👋 Exiting');
process.exit(0);
})
.catch((error) => {
console.error('Fatal error:', error);
process.exit(1);
});
16 changes: 13 additions & 3 deletions node/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { VercelAIAnthropicStreamingProvider } from './providers/vercel-ai-anthro
import { VercelGenerateObjectProvider } from './providers/vercel-generate-object.js';
import { VercelStreamObjectProvider } from './providers/vercel-stream-object.js';
import { MastraProvider } from './providers/mastra.js';
import { MastraOtelProvider } from './providers/mastra-otel.js';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
Expand Down Expand Up @@ -142,7 +143,8 @@ function displayProviders(mode?: string): Map<string, string> {
['13', 'Vercel streamObject (OpenAI)'],
['14', 'Vercel AI SDK (Anthropic)'],
['15', 'Vercel AI SDK Streaming (Anthropic)'],
['17', 'Mastra (OpenAI) - Manual Instrumentation']
['17', 'Mastra (OpenAI) - Manual Instrumentation'],
['18', 'Mastra (OpenAI) - OTEL Instrumentation']
]);

// Filter providers for embeddings mode
Expand Down Expand Up @@ -186,7 +188,7 @@ function displayProviders(mode?: string): Map<string, string> {
async function getProviderChoice(allowModeChange: boolean = false, allowAll: boolean = false): Promise<string> {
return new Promise((resolve) => {
const askForChoice = () => {
let prompt = '\nSelect a provider (1-17)';
let prompt = '\nSelect a provider (1-18)';
if (allowAll) {
prompt += ', \'a\' for all providers';
}
Expand All @@ -197,7 +199,7 @@ async function getProviderChoice(allowModeChange: boolean = false, allowAll: boo

rl.question(prompt, (choice) => {
choice = choice.trim().toLowerCase();
if (['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12', '13', '14', '15', '16', '17'].includes(choice)) {
if (['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12', '13', '14', '15', '16', '17', '18'].includes(choice)) {
clearScreen();
resolve(choice);
} else if (allowAll && choice === 'a') {
Expand Down Expand Up @@ -284,6 +286,14 @@ function createProvider(choice: string, enableThinking: boolean = false, thinkin
return new OpenAITranscriptionProvider(posthog, aiSessionId);
case '17':
return new MastraProvider(posthog, aiSessionId);
case '18':
return new MastraOtelProvider(
posthog,
aiSessionId,
process.env.POSTHOG_HOST || 'http://localhost:8000',
process.env.POSTHOG_PROJECT_ID || '1',
process.env.POSTHOG_API_KEY!
);
default:
throw new Error('Invalid provider choice');
}
Expand Down
Loading
Loading