From 597fb4711a8c83719ad90263fae29049d98a5ed4 Mon Sep 17 00:00:00 2001 From: Stan Girard Date: Wed, 7 Feb 2024 20:44:37 -0800 Subject: [PATCH] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20telemetry=20(#2169)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Description Please include a summary of the changes and the related issue. Please also include relevant motivation and context. ## Checklist before requesting a review Please delete options that are not relevant. - [ ] My code follows the style guidelines of this project - [ ] I have performed a self-review of my code - [ ] I have commented hard-to-understand areas - [ ] I have ideally added tests that prove my fix is effective or that my feature works - [ ] New and existing unit tests pass locally with my changes - [ ] Any dependent changes have been merged ## Screenshots (if appropriate): --- .env.example | 1 + backend/main.py | 9 +++ .../modules/brain/controller/brain_routes.py | 3 +- .../modules/chat/controller/chat_routes.py | 15 +++-- .../upload/controller/upload_routes.py | 3 +- backend/packages/utils/telemetry.py | 60 +++++++++++++++++++ supabase/functions/telemetry/index.ts | 46 ++++++++++++++ 7 files changed, 129 insertions(+), 8 deletions(-) create mode 100644 backend/packages/utils/telemetry.py create mode 100644 supabase/functions/telemetry/index.ts diff --git a/.env.example b/.env.example index 3ab990431..b32122ca9 100644 --- a/.env.example +++ b/.env.example @@ -34,6 +34,7 @@ PG_DATABASE_URL=notimplementedyet ANTHROPIC_API_KEY=null JWT_SECRET_KEY=super-secret-jwt-token-with-at-least-32-characters-long AUTHENTICATE=true +TELEMETRY_ENABLED=true CELERY_BROKER_URL=redis://redis:6379/0 CELEBRY_BROKER_QUEUE_NAME=quivr-preview.fifo diff --git a/backend/main.py b/backend/main.py index 86a11021a..9172f6e62 100644 --- a/backend/main.py +++ b/backend/main.py @@ -23,6 +23,7 @@ from modules.prompt.controller import prompt_router from modules.upload.controller import upload_router from modules.user.controller import user_router from packages.utils import handle_request_validation_error +from packages.utils.telemetry import send_telemetry from routes.crawl_routes import crawl_router from routes.subscription_routes import subscription_router from sentry_sdk.integrations.fastapi import FastApiIntegration @@ -79,6 +80,14 @@ async def http_exception_handler(_, exc): handle_request_validation_error(app) +if os.getenv("TELEMETRY_ENABLED") == "true": + logger.info("Telemetry enabled, we use telemetry to collect anonymous usage data.") + logger.info( + "To disable telemetry, set the TELEMETRY_ENABLED environment variable to false." + ) + send_telemetry("booting", {"status": "ok"}) + + if __name__ == "__main__": # run main.py to debug backend import uvicorn diff --git a/backend/modules/brain/controller/brain_routes.py b/backend/modules/brain/controller/brain_routes.py index da7dbca5b..1627c3e54 100644 --- a/backend/modules/brain/controller/brain_routes.py +++ b/backend/modules/brain/controller/brain_routes.py @@ -20,6 +20,7 @@ from modules.brain.service.integration_brain_service import ( ) from modules.prompt.service.prompt_service import PromptService from modules.user.entity.user_identity import UserIdentity +from packages.utils.telemetry import send_telemetry from repository.brain import get_question_context_from_brain logger = get_logger(__name__) @@ -105,7 +106,7 @@ async def create_new_brain( status_code=429, detail=f"Maximum number of brains reached ({user_settings.get('max_brains', 5)}).", ) - + send_telemetry("create_brain", {"brain_name": brain.name}) new_brain = brain_service.create_brain( brain=brain, user_id=current_user.id, diff --git a/backend/modules/chat/controller/chat_routes.py b/backend/modules/chat/controller/chat_routes.py index 77aefa996..25be64325 100644 --- a/backend/modules/chat/controller/chat_routes.py +++ b/backend/modules/chat/controller/chat_routes.py @@ -26,6 +26,7 @@ from modules.chat.entity.chat import Chat from modules.chat.service.chat_service import ChatService from modules.notification.service.notification_service import NotificationService from modules.user.entity.user_identity import UserIdentity +from packages.utils.telemetry import send_telemetry from vectorstore.supabase import CustomSupabaseVectorStore logger = get_logger(__name__) @@ -103,6 +104,8 @@ def get_answer_generator( model_name=model_to_use.name, ) + send_telemetry("question_asked", {"model_name": model_to_use.name}) + gpt_answer_generator = chat_instance.get_answer_generator( chat_id=str(chat_id), model=model_to_use.name, @@ -205,9 +208,9 @@ async def create_question_handler( request: Request, chat_question: ChatQuestion, chat_id: UUID, - brain_id: NullableUUID - | UUID - | None = Query(..., description="The ID of the brain"), + brain_id: NullableUUID | UUID | None = Query( + ..., description="The ID of the brain" + ), current_user: UserIdentity = Depends(get_current_user), ): try: @@ -238,9 +241,9 @@ async def create_stream_question_handler( request: Request, chat_question: ChatQuestion, chat_id: UUID, - brain_id: NullableUUID - | UUID - | None = Query(..., description="The ID of the brain"), + brain_id: NullableUUID | UUID | None = Query( + ..., description="The ID of the brain" + ), current_user: UserIdentity = Depends(get_current_user), ) -> StreamingResponse: chat_instance = BrainfulChat() diff --git a/backend/modules/upload/controller/upload_routes.py b/backend/modules/upload/controller/upload_routes.py index 9227bac82..a6c0cf43f 100644 --- a/backend/modules/upload/controller/upload_routes.py +++ b/backend/modules/upload/controller/upload_routes.py @@ -21,6 +21,7 @@ from modules.notification.entity.notification import NotificationsStatusEnum from modules.notification.service.notification_service import NotificationService from modules.user.entity.user_identity import UserIdentity from packages.files.file import convert_bytes, get_file_size +from packages.utils.telemetry import send_telemetry from repository.files.upload_file import upload_file_storage logger = get_logger(__name__) @@ -55,7 +56,7 @@ async def upload_file( user_settings = user_daily_usage.get_user_settings() remaining_free_space = user_settings.get("max_brain_size", 1000000000) - + send_telemetry("upload_file", {"file_name": uploadFile.filename}) file_size = get_file_size(uploadFile) if remaining_free_space - file_size < 0: message = f"Brain will exceed maximum capacity. Maximum file allowed is : {convert_bytes(remaining_free_space)}" diff --git a/backend/packages/utils/telemetry.py b/backend/packages/utils/telemetry.py new file mode 100644 index 000000000..b256ff76c --- /dev/null +++ b/backend/packages/utils/telemetry.py @@ -0,0 +1,60 @@ +import hashlib +import json +import os +import threading + +import httpx + +# Assume these are your Supabase Function endpoint and any necessary headers +TELEMETRY_URL = "https://ovbvcnwemowuuuaebizd.supabase.co/functions/v1/telemetry" +HEADERS = { + "Content-Type": "application/json", +} + + +def generate_machine_key(): + # Get the OpenAI API key from the environment variables + seed = os.getenv("OPENAI_API_KEY") + + # Use SHA-256 hash to generate a unique key from the seed + unique_key = hashlib.sha256(seed.encode()).hexdigest() + + return unique_key + + +def send_telemetry(event_name: str, event_data: dict): + # Generate a unique machine key + machine_key = generate_machine_key() + + # Prepare the payload + payload = json.dumps( + { + "anonymous_identifier": machine_key, + "event_name": event_name, + "event_data": event_data, + } + ) + + # Send the telemetry data + with httpx.Client() as client: + _ = client.post(TELEMETRY_URL, headers=HEADERS, data=payload) + + +def maybe_send_telemetry(event_name: str, event_data: dict): + enable_telemetry = os.getenv("TELEMETRY_ENABLED", "false") + + if enable_telemetry.lower() != "true": + return + + threading.Thread(target=send_telemetry, args=(event_name, event_data)).start() + + +async def main(): + await send_telemetry("user_login", {"login_success": True}) + + +# Run the example +if __name__ == "__main__": + import asyncio + + asyncio.run(main()) diff --git a/supabase/functions/telemetry/index.ts b/supabase/functions/telemetry/index.ts new file mode 100644 index 000000000..00c6ef267 --- /dev/null +++ b/supabase/functions/telemetry/index.ts @@ -0,0 +1,46 @@ +import { createClient } from "https://esm.sh/@supabase/supabase-js@2"; + +Deno.serve(async (req: Request) => { + console.log("Received request"); + + const authHeader = req.headers.get("Authorization")!; + const supabaseClient = createClient( + Deno.env.get("SUPABASE_URL") ?? "", + Deno.env.get("SUPABASE_ANON_KEY") ?? "", + { global: { headers: { Authorization: authHeader } } } + ); + + // Parse the request body to get event data and the anonymous identifier + const { anonymous_identifier, event_name, event_data } = await req.json(); + + console.log( + `Parsed request body: ${JSON.stringify({ + anonymous_identifier, + event_name, + event_data, + })}` + ); + + // Insert the telemetry data along with the anonymous identifier into the Supabase table + const { data, error } = await supabaseClient + .from("telemetry") + .insert([{ anonymous_identifier, event_name, event_data }]); + + if (error) { + console.error(`Error inserting data into Supabase: ${error.message}`); + return new Response(JSON.stringify({ error: error.message }), { + status: 400, + headers: { + "Content-Type": "application/json", + }, + }); + } + + console.log("Successfully inserted data into Supabase"); + return new Response(JSON.stringify({ message: "Telemetry logged", data }), { + status: 200, + headers: { + "Content-Type": "application/json", + }, + }); +});