From d05819ed3fefc6ea86022c9e7679ed6b7bdcfc22 Mon Sep 17 00:00:00 2001 From: "Vincent (Wen Yu) Ge" Date: Tue, 21 Oct 2025 20:51:30 -0400 Subject: [PATCH 1/4] Update capture tutorial --- contents/tutorials/api-capture-events.mdx | 463 +++++++++++++++++++++- 1 file changed, 458 insertions(+), 5 deletions(-) diff --git a/contents/tutorials/api-capture-events.mdx b/contents/tutorials/api-capture-events.mdx index fdb26937a4ab..9b5cad6a0219 100644 --- a/contents/tutorials/api-capture-events.mdx +++ b/contents/tutorials/api-capture-events.mdx @@ -11,14 +11,40 @@ tags: - product analytics - product os --- -export const apiEventsLight = "https://res.cloudinary.com/dmukukwp6/image/upload/v1710055416/posthog.com/contents/images/tutorials/api-capture-events/api-events-light-mode.png" -export const apiEventsDark = "https://res.cloudinary.com/dmukukwp6/image/upload/v1710055416/posthog.com/contents/images/tutorials/api-capture-events/api-events-dark-mode.png" +export const apiEventsLight = "https://res.cloudinary.com/dmukukwp6/image/upload/w_1600,c_limit,q_auto,f_auto/view_event_light_30a37c70a8.png" +export const apiEventsDark = "https://res.cloudinary.com/dmukukwp6/image/upload/w_1600,c_limit,q_auto,f_auto/view_event_dark_97fe1d29d5.png" PostHog provides [libraries](/docs/integrate?tab=sdks) that make it easy to capture events in popular languages. These libraries are basically wrappers around the API. They handle and automate common tasks like capturing pageviews. Using the API directly allows for any language that can send requests to capture events, or completely customize your implementation. Using the API to capture events directly also gives you a better understanding of [PostHog's event-based data structures](/docs/how-posthog-works/data-model) which is abstracted if you use a library. +## Base URL + +The base URL of your PostHog depends on the region of your PostHog project: + + + +```bash file=US +https://us.i.posthog.com +``` + +```bash file=EU +https://eu.i.posthog.com +``` + + + +## Capture endpoint + +PostHog captures events through `/i/v0/e/` endpoint of your project region. + +For your PostHog project (if you're authenticated on [PostHog](https://app.posthog.com/)), the fill URL is: + +```bash +/i/v0/e/ +``` + ## Authenticating with the project API key The first thing needed, like the [basic GET request tutorial](/tutorials/api-get-insights-persons), is to authenticate ourselves in the API. Unlike in the GET request tutorial, we can use the project API key (the same key you use to initialize a PostHog library). This can be found in your project settings. @@ -61,9 +87,36 @@ response = requests.post(url, headers=headers, json=body) print(response.json()) ``` +```js file=NodeJS +require('dotenv').config(); + +const headers = { + "Content-Type": "application/json", +}; + +const body = { + "api_key": "", + "event": "request", + "properties": { + "distinct_id": "ian@posthog.com" + } +}; + +const url = `${process.env.POSTHOG_API_HOST}/i/v0/e/`; + +fetch(url, { + method: 'POST', + headers: headers, + body: JSON.stringify(body) +}) +.then(response => response.json()) +.then(data => console.log(data)) +.catch(error => console.error('Error:', error)); +``` + -Once you've done that, you should see the event in your PostHog instance. +Once you've done that, you should see the event in your PostHog project's [activity tab](https://app.posthog.com/activity/explore). @@ -115,9 +168,40 @@ response = requests.post(url, headers=headers, json=body) print(response.json()) ``` +```js file=NodeJS +// Load environment variables from .env file +require('dotenv').config(); + +const headers = { + "Content-Type": "application/json", +}; + +const body = { + "api_key": process.env.POSTHOG_API_KEY, + "event": "big_request", + "timestamp": "2022-10-21 09:03:11.913767", + "properties": { + "distinct_id": "ian@posthog.com", + "request_size": "big", + "api_request": true + } +}; + +const url = `${process.env.POSTHOG_API_HOST}/i/v0/e/`; + +fetch(url, { + method: 'POST', + headers: headers, + body: JSON.stringify(body) +}) +.then(response => response.json()) +.then(data => console.log(data)) +.catch(error => console.error('Error:', error)); +``` + -You can also batch these requests together by sending a list of events to the `/batch/` endpoint. This is useful for limiting the number of requests you make. Events can be held, then sent as a batch. Our [Node.js library](/docs/integrate/server/node) does this automatically, and we use batching to process events. +You can also batch these requests together by sending a list of events to the `/batch/` endpoint. This is useful for limiting the number of requests you make. Events can be held, then sent as a batch. PostHog SDKs do this automatically, and we use batching to process events. @@ -176,6 +260,46 @@ response = requests.post(url, headers=headers, json=body) print(response.json()) ``` +```js file=NodeJS +// Load environment variables from .env file +require('dotenv').config(); + +const headers = { + "Content-Type": "application/json", +}; + +const body = { + "api_key": "", + "batch": [ + { + "event": "batched_event", + "properties" : { + "distinct_id": "test-user-javascript", + "number_in_batch": 1 + } + }, + { + "event": "batched_event", + "properties" : { + "distinct_id": "test-user-javascript", + "number_in_batch": 2 + } + } + ] +}; + +const url = `/batch/`; + +fetch(url, { + method: 'POST', + headers: headers, + body: JSON.stringify(body) +}) +.then(response => response.json()) +.then(data => console.log(data)) +.catch(error => console.error('Error:', error)); +``` + ## Identifying and aliasing users @@ -219,6 +343,35 @@ response = requests.post(url, headers=headers, json=body) print(response.json()) ``` +```js file=NodeJS +// Load environment variables from .env file +require('dotenv').config(); + +const headers = { + "Content-Type": "application/json", +}; + +const body = { + "api_key": "", + "distinct_id": "test-user-javascript", + "$set": { + "email": "test-user-javascript@example.com", + "is_cool": true + }, + "event": "$identify" +}; + +const url = "/i/v0/e/"; + +fetch(url, { + method: 'POST', + headers: headers, + body: JSON.stringify(body) +}) +.then(response => response.json()) +.then(data => console.log(data)) +.catch(error => console.error('Error:', error)); +``` @@ -258,15 +411,315 @@ body = { response = requests.post(url, headers=headers, json=body) +print(response.json()) +``` +```js file=NodeJS +// Load environment variables from .env file +require('dotenv').config(); + +const headers = { + "Content-Type": "application/json", +}; + +const body = { + "api_key": "", + "properties": { + "distinct_id": "test-user-javascript", + "alias": "test-user-javascript-alias" + }, + "event": "$create_alias" +}; + +const url = "/i/v0/e/"; + +fetch(url, { + method: 'POST', + headers: headers, + body: JSON.stringify(body) +}) +.then(response => response.json()) +.then(data => console.log(data)) +.catch(error => console.error('Error:', error)); +``` + + +## Capturing errors + +Everything is an event in PostHog. [Error tracking](/docs/error-tracking) is no different. You can manually capture errors by sending an `$exception` event with the following properties: + +| Property | Description | +|----------|-------------| +| `$exception_list` | A list of exception objects with detailed information about each error. Each exception can include a `type`, `value`, `mechanism`, `module`, and a `stacktrace` with `frames` and `type`. You can find the expected schema as types for both [exception](https://github.com/PostHog/posthog/blob/master/rust/cymbal/src/types/mod.rs#L39) and [stack frames](https://github.com/PostHog/posthog/blob/master/rust/cymbal/src/langs/custom.rs#L12) in our Rust repo | +| `$exception_fingerprint` | (Optional) The identifier used to group issues. If not set, a unique hash based on the exception pattern will be generated during ingestion | + +Here's an example of how to capture an error: + + + +```bash +#!/bin/bash + +# Load environment variables from .env file +if [ -f .env ]; then + export $(cat .env | grep -v '^#' | xargs) +fi + +curl -X POST "/i/v0/e/" \ + -H "Content-Type: application/json" \ + -d "{ + \"api_key\": \"\", + \"event\": \"\$exception\", + \"properties\": { + \"distinct_id\": \"ian@posthog.com\", + \"\$exception_list\": [{ + \"type\": \"ScriptError\", + \"value\": \"Command not found: fake_command\", + \"mechanism\": { + \"handled\": true, + \"synthetic\": false + }, + \"stacktrace\": { + \"type\": \"raw\", + \"frames\": [ + { + \"platform\": \"custom\", + \"lang\": \"bash\", + \"function\": \"main\", + \"filename\": \"basic-exception.sh\", + \"lineno\": 15, + \"colno\": 1, + \"module\": \"script_execution\", + \"resolved\": true, + \"in_app\": true + }, + { + \"platform\": \"custom\", + \"lang\": \"bash\", + \"function\": \"execute_command\", + \"filename\": \"utils.sh\", + \"lineno\": 42, + \"colno\": 5, + \"module\": \"command_handler\", + \"resolved\": true, + \"in_app\": true + }, + { + \"platform\": \"custom\", + \"lang\": \"bash\", + \"function\": \"error_event_bash\", + \"filename\": \"error_handler.sh\", + \"lineno\": 8, + \"colno\": 12, + \"module\": \"error_tracking\", + \"resolved\": false, + \"in_app\": false + } + ] + } + }], + \"\$exception_fingerprint\": \"$(echo 'Command not found: fake_command' | md5sum | cut -c1-32)\" + } + }" + +``` + +```python +import requests +import os +import traceback +import hashlib +import time +from dotenv import load_dotenv + +# Load environment variables from .env file +load_dotenv() + +url = "/i/v0/e/" + +headers = { + "Content-Type": "application/json", +} + +# Create a fake exception for demonstration +try: + # Simulate an error_event_python + raise ValueError("error_event_python: This is a simulated error for testing") +except Exception as e: + # Get the current traceback + tb = traceback.format_exc() + + # Create exception fingerprint + fingerprint = hashlib.md5(str(e).encode()).hexdigest() + + body = { + "api_key": "", + "event": "$exception", + "properties": { + "distinct_id": "ian@posthog.com", + "$exception_list": [{ + "type": type(e).__name__, + "value": str(e), + "mechanism": { + "handled": True, + "synthetic": False + }, + "stacktrace": { + "type": "raw", + "frames": [ + { + "platform": "custom", + "lang": "python", + "function": "error_event_python", + "filename": "basic-exception.py", + "lineno": 15, + "colno": 1, + "module": "exception_handler", + "resolved": True, + "in_app": True + }, + { + "platform": "custom", + "lang": "python", + "function": "simulate_error", + "filename": "error_simulator.py", + "lineno": 8, + "colno": 5, + "module": "testing", + "resolved": True, + "in_app": True + }, + { + "platform": "custom", + "lang": "python", + "function": "main", + "filename": "app.py", + "lineno": 42, + "colno": 12, + "module": "application", + "resolved": False, + "in_app": False + } + ] + } + }], + "$exception_fingerprint": fingerprint + } + } + +response = requests.post(url, headers=headers, json=body) + print(response.json()) ``` +```js file=NodeJS +// Load environment variables from .env file +require('dotenv').config(); +const crypto = require('crypto'); + +const headers = { + "Content-Type": "application/json", +}; + +// Create a fake exception for demonstration +try { + // Simulate an error_event_javascript + throw new Error('error_event_javascript: This is a simulated error for testing'); +} catch (error) { + // Create exception fingerprint + const fingerprint = crypto.createHash('md5').update(error.message).digest('hex'); + + const body = { + "api_key": "", + "event": "$exception", + "properties": { + "distinct_id": "ian@posthog.com", + "$exception_list": [{ + "type": error.name, + "value": error.message, + "mechanism": { + "handled": true, + "synthetic": false + }, + "stacktrace": { + "type": "raw", + "frames": [ + { + "platform": "custom", + "lang": "javascript", + "function": "error_event_javascript", + "filename": "basic-exception.js", + "lineno": 8, + "colno": 1, + "module": "exception_handler", + "resolved": true, + "in_app": true + }, + { + "platform": "custom", + "lang": "javascript", + "function": "simulateError", + "filename": "error-simulator.js", + "lineno": 15, + "colno": 5, + "module": "testing", + "resolved": true, + "in_app": true + }, + { + "platform": "custom", + "lang": "javascript", + "function": "main", + "filename": "app.js", + "lineno": 42, + "colno": 12, + "module": "application", + "resolved": false, + "in_app": false + } + ] + } + }], + "$exception_fingerprint": fingerprint + } + }; + + const url = "/i/v0/e/"; + + fetch(url, { + method: 'POST', + headers: headers, + body: JSON.stringify(body) + }) + .then(response => response.json()) + .then(data => console.log(data)) + .catch(err => console.error('Error:', err)); +} +``` +While possible, we strongly recommend you stick to using our [error tracking](/docs/error-tracking/installation) SDKs instead of manually capturing errors for features like accurate fingerprinting, source-map support, release tracking, and more. + +## Capturing LLM analytics events + +It's also possible to capture LLM analytics events using the API. If you're using a language without [SDK support for LLM analytics](/docs/llm-analytics/installation), you can use the API to capture events. + +To capture LLM analytics, you need to capture 4 types of events: + +| Event type | Event name | Documentation | +|--------------|-------------------|---------------------------------------------| +| Generations | `$ai_generation` | [What are generations?](/docs/llm-analytics/generations) | +| Spans | `$ai_span` | [What are spans?](/docs/llm-analytics/spans) | +| Traces | `$ai_trace` | [What are traces?](/docs/llm-analytics/traces) | +| Embeddings | `$ai_embedding` | [What are embeddings?](/docs/llm-analytics/embeddings) | + +You can find more information in our [manual capture guide](/docs/llm-analytics/manual-capture). + ## Further reading - [How to use the PostHog API to get insights and persons](/tutorials/api-get-insights-persons) - [Documentation on our event capture API endpoint](/docs/api/capture) - [How to evaluate and update feature flags with the PostHog API](/tutorials/api-feature-flags) +- [Manual error tracking capture](/docs/error-tracking/installation/manual) +- [Manual LLM analytics capture](/docs/llm-analytics/manual-capture) \ No newline at end of file From 60badeb12ff9013c755552e0d0ece3d840d1f001 Mon Sep 17 00:00:00 2001 From: "Vincent (Wen Yu) Ge" Date: Tue, 21 Oct 2025 20:52:36 -0400 Subject: [PATCH 2/4] batching base url --- contents/tutorials/api-capture-events.mdx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/contents/tutorials/api-capture-events.mdx b/contents/tutorials/api-capture-events.mdx index 9b5cad6a0219..3430f6247933 100644 --- a/contents/tutorials/api-capture-events.mdx +++ b/contents/tutorials/api-capture-events.mdx @@ -45,6 +45,12 @@ For your PostHog project (if you're authenticated on [PostHog](https://app.posth /i/v0/e/ ``` +You can also use the `/batch` endpoint to capture multiple events in one request. We cover this in the [batching events section](#batching-events). + +```bash +/batch/ +``` + ## Authenticating with the project API key The first thing needed, like the [basic GET request tutorial](/tutorials/api-get-insights-persons), is to authenticate ourselves in the API. Unlike in the GET request tutorial, we can use the project API key (the same key you use to initialize a PostHog library). This can be found in your project settings. From b1066c2c5e541b02592194ac74fe9775930719a1 Mon Sep 17 00:00:00 2001 From: "Vincent (Wen Yu) Ge" Date: Wed, 22 Oct 2025 11:59:48 -0400 Subject: [PATCH 3/4] Updated tutorial --- contents/tutorials/api-capture-events.mdx | 113 +++++++++------------- 1 file changed, 44 insertions(+), 69 deletions(-) diff --git a/contents/tutorials/api-capture-events.mdx b/contents/tutorials/api-capture-events.mdx index 3430f6247933..e83949a06c55 100644 --- a/contents/tutorials/api-capture-events.mdx +++ b/contents/tutorials/api-capture-events.mdx @@ -1,6 +1,6 @@ --- title: Using the PostHog API to capture events -date: 2023-02-09T00:00:00.000Z +date: 2025-10-22T00:00:00.000Z author: - ian-vanagas showTitle: true @@ -175,8 +175,6 @@ print(response.json()) ``` ```js file=NodeJS -// Load environment variables from .env file -require('dotenv').config(); const headers = { "Content-Type": "application/json", @@ -267,9 +265,6 @@ print(response.json()) ``` ```js file=NodeJS -// Load environment variables from .env file -require('dotenv').config(); - const headers = { "Content-Type": "application/json", }; @@ -350,9 +345,6 @@ response = requests.post(url, headers=headers, json=body) print(response.json()) ``` ```js file=NodeJS -// Load environment variables from .env file -require('dotenv').config(); - const headers = { "Content-Type": "application/json", }; @@ -420,9 +412,6 @@ response = requests.post(url, headers=headers, json=body) print(response.json()) ``` ```js file=NodeJS -// Load environment variables from .env file -require('dotenv').config(); - const headers = { "Content-Type": "application/json", }; @@ -463,70 +452,62 @@ Here's an example of how to capture an error: ```bash -#!/bin/bash - -# Load environment variables from .env file -if [ -f .env ]; then - export $(cat .env | grep -v '^#' | xargs) -fi - curl -X POST "/i/v0/e/" \ -H "Content-Type: application/json" \ - -d "{ - \"api_key\": \"\", - \"event\": \"\$exception\", - \"properties\": { - \"distinct_id\": \"ian@posthog.com\", - \"\$exception_list\": [{ - \"type\": \"ScriptError\", - \"value\": \"Command not found: fake_command\", - \"mechanism\": { - \"handled\": true, - \"synthetic\": false + -d '{ + "api_key": "", + "event": "$exception", + "properties": { + "distinct_id": "ian@posthog.com", + "$exception_list": [{ + "type": "ScriptError", + "value": "Command not found: fake_command", + "mechanism": { + "handled": true, + "synthetic": false }, - \"stacktrace\": { - \"type\": \"raw\", - \"frames\": [ + "stacktrace": { + "type": "raw", + "frames": [ { - \"platform\": \"custom\", - \"lang\": \"bash\", - \"function\": \"main\", - \"filename\": \"basic-exception.sh\", - \"lineno\": 15, - \"colno\": 1, - \"module\": \"script_execution\", - \"resolved\": true, - \"in_app\": true + "platform": "custom", + "lang": "bash", + "function": "main", + "filename": "basic-exception.sh", + "lineno": 15, + "colno": 1, + "module": "script_execution", + "resolved": true, + "in_app": true }, { - \"platform\": \"custom\", - \"lang\": \"bash\", - \"function\": \"execute_command\", - \"filename\": \"utils.sh\", - \"lineno\": 42, - \"colno\": 5, - \"module\": \"command_handler\", - \"resolved\": true, - \"in_app\": true + "platform": "custom", + "lang": "bash", + "function": "execute_command", + "filename": "utils.sh", + "lineno": 42, + "colno": 5, + "module": "command_handler", + "resolved": true, + "in_app": true }, { - \"platform\": \"custom\", - \"lang\": \"bash\", - \"function\": \"error_event_bash\", - \"filename\": \"error_handler.sh\", - \"lineno\": 8, - \"colno\": 12, - \"module\": \"error_tracking\", - \"resolved\": false, - \"in_app\": false + "platform": "custom", + "lang": "bash", + "function": "error_event_bash", + "filename": "error_handler.sh", + "lineno": 8, + "colno": 12, + "module": "error_tracking", + "resolved": false, + "in_app": false } ] } }], - \"\$exception_fingerprint\": \"$(echo 'Command not found: fake_command' | md5sum | cut -c1-32)\" + "$exception_fingerprint": } - }" - + }' ``` ```python @@ -535,10 +516,6 @@ import os import traceback import hashlib import time -from dotenv import load_dotenv - -# Load environment variables from .env file -load_dotenv() url = "/i/v0/e/" @@ -618,8 +595,6 @@ print(response.json()) ``` ```js file=NodeJS -// Load environment variables from .env file -require('dotenv').config(); const crypto = require('crypto'); const headers = { From 3197cdf9aa3da9d8b097c2b90c2615bf9b3ee259 Mon Sep 17 00:00:00 2001 From: "Vincent (Wen Yu) Ge" Date: Wed, 22 Oct 2025 12:01:16 -0400 Subject: [PATCH 4/4] They should all be from ian, remove env. vars --- contents/tutorials/api-capture-events.mdx | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/contents/tutorials/api-capture-events.mdx b/contents/tutorials/api-capture-events.mdx index e83949a06c55..2e92c2ec227e 100644 --- a/contents/tutorials/api-capture-events.mdx +++ b/contents/tutorials/api-capture-events.mdx @@ -108,7 +108,7 @@ const body = { } }; -const url = `${process.env.POSTHOG_API_HOST}/i/v0/e/`; +const url = "/i/v0/e/"; fetch(url, { method: 'POST', @@ -181,7 +181,7 @@ const headers = { }; const body = { - "api_key": process.env.POSTHOG_API_KEY, + "api_key": "", "event": "big_request", "timestamp": "2022-10-21 09:03:11.913767", "properties": { @@ -191,7 +191,7 @@ const body = { } }; -const url = `${process.env.POSTHOG_API_HOST}/i/v0/e/`; +const url = "/i/v0/e/"; fetch(url, { method: 'POST', @@ -275,14 +275,14 @@ const body = { { "event": "batched_event", "properties" : { - "distinct_id": "test-user-javascript", + "distinct_id": "ian@posthog.com", "number_in_batch": 1 } }, { "event": "batched_event", "properties" : { - "distinct_id": "test-user-javascript", + "distinct_id": "ian@posthog.com", "number_in_batch": 2 } } @@ -351,9 +351,9 @@ const headers = { const body = { "api_key": "", - "distinct_id": "test-user-javascript", + "distinct_id": "ian@posthog.com", "$set": { - "email": "test-user-javascript@example.com", + "email": "ian@posthog.com", "is_cool": true }, "event": "$identify" @@ -419,8 +419,8 @@ const headers = { const body = { "api_key": "", "properties": { - "distinct_id": "test-user-javascript", - "alias": "test-user-javascript-alias" + "distinct_id": "ian@posthog.com", + "alias": "ian2@posthog.com" }, "event": "$create_alias" };