Skip to content
Draft
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
5 changes: 5 additions & 0 deletions .changeset/quiet-llamas-join.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'pypi/posthog': patch
---

Generate SDK-created event, personless, and AI telemetry IDs as UUID v7.
31 changes: 31 additions & 0 deletions posthog/_uuid.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
"""UUID generation helpers."""

import os
import time
import uuid


_UUID7_RANDOM_BITS = 74
_UUID7_RANDOM_MASK = (1 << _UUID7_RANDOM_BITS) - 1
_UUID7_TIMESTAMP_MASK = (1 << 48) - 1


def uuid7() -> str:
"""Return a UUID v7 string.

Python 3.14+ includes ``uuid.uuid7`` in the standard library. Older
supported runtimes do not, so fall back to a small RFC 9562-compatible
implementation using the current Unix epoch milliseconds and 74 random bits.
"""

stdlib_uuid7 = getattr(uuid, "uuid7", None)
if stdlib_uuid7 is not None:
return str(stdlib_uuid7())

unix_ts_ms = int(time.time() * 1000) & _UUID7_TIMESTAMP_MASK
random_bits = int.from_bytes(os.urandom(10), "big") & _UUID7_RANDOM_MASK
rand_a = random_bits >> 62
rand_b = random_bits & ((1 << 62) - 1)

uuid_int = (unix_ts_ms << 80) | (0x7 << 76) | (rand_a << 64) | (0b10 << 62) | rand_b
Comment thread
marandaneto marked this conversation as resolved.
return str(uuid.UUID(int=uuid_int))
6 changes: 3 additions & 3 deletions posthog/ai/anthropic/anthropic.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
)

import time
import uuid
from typing import Any, Dict, List, Optional

from posthog.ai.types import StreamingContentBlock, TokenUsage, ToolInProgress
Expand All @@ -24,6 +23,7 @@
)
from posthog.ai.sanitization import sanitize_anthropic
from posthog.client import Client as PostHogClient
from posthog._uuid import uuid7
from posthog import setup


Expand Down Expand Up @@ -70,7 +70,7 @@ def create(
"""

if posthog_trace_id is None:
posthog_trace_id = str(uuid.uuid4())
posthog_trace_id = uuid7()

if kwargs.get("stream", False):
return self._create_streaming(
Expand Down Expand Up @@ -119,7 +119,7 @@ def stream(
A streaming iterator yielding Anthropic events.
"""
if posthog_trace_id is None:
posthog_trace_id = str(uuid.uuid4())
posthog_trace_id = uuid7()

return self._create_streaming(
posthog_distinct_id,
Expand Down
6 changes: 3 additions & 3 deletions posthog/ai/anthropic/anthropic_async.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
)

import time
import uuid
from typing import Any, Dict, List, Optional

from posthog import setup
Expand All @@ -26,6 +25,7 @@
)
from posthog.ai.sanitization import sanitize_anthropic
from posthog.client import Client as PostHogClient
from posthog._uuid import uuid7


class AsyncAnthropic(anthropic.AsyncAnthropic):
Expand Down Expand Up @@ -71,7 +71,7 @@ async def create(
"""

if posthog_trace_id is None:
posthog_trace_id = str(uuid.uuid4())
posthog_trace_id = uuid7()

if kwargs.get("stream", False):
return await self._create_streaming(
Expand Down Expand Up @@ -120,7 +120,7 @@ async def stream(
An async streaming iterator yielding Anthropic events.
"""
if posthog_trace_id is None:
posthog_trace_id = str(uuid.uuid4())
posthog_trace_id = uuid7()

return await self._create_streaming(
posthog_distinct_id,
Expand Down
4 changes: 2 additions & 2 deletions posthog/ai/claude_agent_sdk/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@

import logging
import time
import uuid
from typing import Any, Callable, Dict, List, Optional, Union

try:
Expand All @@ -28,6 +27,7 @@
_GenerationTracker,
)
from posthog.client import Client
from posthog._uuid import uuid7

log = logging.getLogger("posthog")

Expand Down Expand Up @@ -91,7 +91,7 @@ def __init__(
groups=posthog_groups,
properties=posthog_properties or {},
)
self._trace_id = posthog_trace_id or str(uuid.uuid4())
self._trace_id = posthog_trace_id or uuid7()
self._distinct_id = posthog_distinct_id
self._extra_props = posthog_properties or {}
self._privacy = posthog_privacy_mode
Expand Down
10 changes: 5 additions & 5 deletions posthog/ai/claude_agent_sdk/processor.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@

import logging
import time
import uuid
from dataclasses import dataclass, field
from typing import Any, Callable, Dict, List, Optional, Union

Expand All @@ -26,6 +25,7 @@

from posthog import setup
from posthog.client import Client
from posthog._uuid import uuid7

log = logging.getLogger("posthog")

Expand All @@ -41,7 +41,7 @@ class _GenerationData:
cache_creation_input_tokens: int = 0
start_time: float = 0.0
end_time: float = 0.0
span_id: str = field(default_factory=lambda: str(uuid.uuid4()))
span_id: str = field(default_factory=lambda: uuid7())
stop_reason: Optional[str] = None


Expand Down Expand Up @@ -240,7 +240,7 @@ async def query(

# Per-call overrides
distinct_id_override = posthog_distinct_id or self._distinct_id
trace_id = posthog_trace_id or str(uuid.uuid4())
trace_id = posthog_trace_id or uuid7()
extra_props = posthog_properties or {}
privacy = (
posthog_privacy_mode
Expand Down Expand Up @@ -480,7 +480,7 @@ def _emit_generation_from_result(

properties: Dict[str, Any] = {
"$ai_trace_id": trace_id,
"$ai_span_id": str(uuid.uuid4()),
"$ai_span_id": uuid7(),
"$ai_span_name": "generation_1",
"$ai_provider": "anthropic",
"$ai_framework": "claude-agent-sdk",
Expand Down Expand Up @@ -534,7 +534,7 @@ def _emit_tool_span(

properties: Dict[str, Any] = {
"$ai_trace_id": trace_id,
"$ai_span_id": str(uuid.uuid4()),
"$ai_span_id": uuid7(),
"$ai_parent_id": parent_span_id,
"$ai_span_name": block.name,
"$ai_span_type": "tool",
Expand Down
4 changes: 2 additions & 2 deletions posthog/ai/gemini/gemini.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import os
import time
import uuid
from typing import Any, Dict, Optional

from posthog.ai.types import TokenUsage, StreamingEventData
Expand Down Expand Up @@ -29,6 +28,7 @@
from posthog.ai.utils import with_privacy_mode
from posthog.ai.sanitization import sanitize_gemini
from posthog.client import Client as PostHogClient
from posthog._uuid import uuid7


class Client:
Expand Down Expand Up @@ -229,7 +229,7 @@ def _merge_posthog_params(
properties.update(call_properties)

if call_trace_id is None:
call_trace_id = str(uuid.uuid4())
call_trace_id = uuid7()

return distinct_id, call_trace_id, properties, privacy_mode, groups

Expand Down
4 changes: 2 additions & 2 deletions posthog/ai/gemini/gemini_async.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import os
import time
import uuid
from typing import Any, Dict, Optional

from posthog.ai.stream import AsyncStreamWrapper
Expand Down Expand Up @@ -30,6 +29,7 @@
from posthog.ai.utils import with_privacy_mode
from posthog.ai.sanitization import sanitize_gemini
from posthog.client import Client as PostHogClient
from posthog._uuid import uuid7


class AsyncClient:
Expand Down Expand Up @@ -230,7 +230,7 @@ def _merge_posthog_params(
properties.update(call_properties)

if call_trace_id is None:
call_trace_id = str(uuid.uuid4())
call_trace_id = uuid7()

return distinct_id, call_trace_id, properties, privacy_mode, groups

Expand Down
8 changes: 4 additions & 4 deletions posthog/ai/openai/openai.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import time
import uuid
from typing import Any, Dict, List, Optional

from posthog.ai.types import TokenUsage
Expand All @@ -25,6 +24,7 @@
)
from posthog.ai.sanitization import sanitize_openai, sanitize_openai_response
from posthog.client import Client as PostHogClient
from posthog._uuid import uuid7
from posthog import setup
from posthog.ai.openai.wrapper_utils import _OpenAIWrapperResource

Expand Down Expand Up @@ -118,7 +118,7 @@ def create(
The OpenAI response, or a streaming iterator when ``stream=True``.
"""
if posthog_trace_id is None:
posthog_trace_id = str(uuid.uuid4())
posthog_trace_id = uuid7()

if kwargs.get("stream", False):
return self._create_streaming(
Expand Down Expand Up @@ -372,7 +372,7 @@ def create(
The OpenAI chat completion, or a streaming iterator when ``stream=True``.
"""
if posthog_trace_id is None:
posthog_trace_id = str(uuid.uuid4())
posthog_trace_id = uuid7()

if kwargs.get("stream", False):
return self._create_streaming(
Expand Down Expand Up @@ -567,7 +567,7 @@ def create(
"""

if posthog_trace_id is None:
posthog_trace_id = str(uuid.uuid4())
posthog_trace_id = uuid7()

start_time = time.time()
response = self._original.create(**kwargs)
Expand Down
12 changes: 6 additions & 6 deletions posthog/ai/openai/openai_async.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import time
import uuid
from typing import Any, Dict, List, Optional

from posthog.ai.stream import AsyncStreamWrapper
Expand Down Expand Up @@ -29,6 +28,7 @@
)
from posthog.ai.sanitization import sanitize_openai, sanitize_openai_response
from posthog.client import Client as PostHogClient
from posthog._uuid import uuid7
from posthog.ai.openai.wrapper_utils import _OpenAIWrapperResource


Expand Down Expand Up @@ -121,7 +121,7 @@ async def create(
The OpenAI response, or an async streaming iterator when ``stream=True``.
"""
if posthog_trace_id is None:
posthog_trace_id = str(uuid.uuid4())
posthog_trace_id = uuid7()

if kwargs.get("stream", False):
return await self._create_streaming(
Expand Down Expand Up @@ -240,7 +240,7 @@ async def _capture_streaming_event(
stop_reason: Optional[str] = None,
):
if posthog_trace_id is None:
posthog_trace_id = str(uuid.uuid4())
posthog_trace_id = uuid7()

# Use model from kwargs, fallback to model from response
model = kwargs.get("model") or model_from_response or "unknown"
Expand Down Expand Up @@ -401,7 +401,7 @@ async def create(
The OpenAI chat completion, or an async streaming iterator when ``stream=True``.
"""
if posthog_trace_id is None:
posthog_trace_id = str(uuid.uuid4())
posthog_trace_id = uuid7()

# If streaming, handle streaming specifically
if kwargs.get("stream", False):
Expand Down Expand Up @@ -535,7 +535,7 @@ async def _capture_streaming_event(
stop_reason: Optional[str] = None,
):
if posthog_trace_id is None:
posthog_trace_id = str(uuid.uuid4())
posthog_trace_id = uuid7()

# Use model from kwargs, fallback to model from response
model = kwargs.get("model") or model_from_response or "unknown"
Expand Down Expand Up @@ -623,7 +623,7 @@ async def create(
"""

if posthog_trace_id is None:
posthog_trace_id = str(uuid.uuid4())
posthog_trace_id = uuid7()

start_time = time.time()
response = await self._original.create(**kwargs)
Expand Down
8 changes: 4 additions & 4 deletions posthog/ai/utils.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import time
import uuid
from typing import Any, Callable, Dict, List, Optional, cast

from posthog import get_tags, identify_context, new_context, tag, contexts
Expand All @@ -12,6 +11,7 @@
)
from posthog.ai.types import FormattedMessage, StreamingEventData, TokenUsage
from posthog.client import Client as PostHogClient
from posthog._uuid import uuid7


_TOKEN_PROPERTY_KEYS = frozenset(
Expand Down Expand Up @@ -384,7 +384,7 @@ def call_llm_and_track_usage(
latency = end_time - start_time

if posthog_trace_id is None:
posthog_trace_id = str(uuid.uuid4())
posthog_trace_id = uuid7()

# Check if we have a real user distinct_id (from param or outer context)
has_person_distinct_id = (
Expand Down Expand Up @@ -534,7 +534,7 @@ async def call_llm_and_track_usage_async(
latency = end_time - start_time

if posthog_trace_id is None:
posthog_trace_id = str(uuid.uuid4())
posthog_trace_id = uuid7()

# Check if we have a real user distinct_id (from param or outer context)
has_person_distinct_id = (
Expand Down Expand Up @@ -683,7 +683,7 @@ def capture_streaming_event(
ph_client: PostHog client instance
event_data: Standardized streaming event data containing all necessary information
"""
trace_id = event_data.get("trace_id") or str(uuid.uuid4())
trace_id = event_data.get("trace_id") or uuid7()

# Build base event properties
event_properties = {
Expand Down
Loading
Loading