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
139 changes: 127 additions & 12 deletions .github/workflows/posthog-upgrade.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ on:
permissions:
contents: read

concurrency:
group: posthog-upgrade-${{ github.event.inputs.package_name }}
cancel-in-progress: false

jobs:
posthog-upgrade:
name: Upgrade PostHog package
Expand Down Expand Up @@ -81,16 +85,117 @@ jobs:
fi
done

- name: Generate branch name
id: generate-branch-name
- name: Generate pull request details
id: generate-pr-details
shell: bash
env:
GH_TOKEN: ${{ steps.upgrader.outputs.token }}
PACKAGE_NAME: ${{ github.event.inputs.package_name }}
PACKAGE_VERSION: ${{ github.event.inputs.package_version }}
run: |
echo "branch_name=${PACKAGE_NAME}-${PACKAGE_VERSION}" >> "$GITHUB_OUTPUT"
TITLE_PREFIX="chore(deps): update ${PACKAGE_NAME} to "

skip_update() {
echo "::notice::$1"
echo "skip=true" >> "$GITHUB_OUTPUT"
exit 0
}

# Serialize writers and reject stale reruns using Python package ordering.
check_version() {
local current_title="$1" current_version older
if [[ "$current_title" != "$TITLE_PREFIX"* ]]; then
echo "::error::Cannot determine the destination PR's package version." >&2
exit 1
fi
current_version="${current_title#"$TITLE_PREFIX"}"
older=$(uv run --no-project --with packaging==25.0 python - "$PACKAGE_VERSION" "$current_version" <<'PY'
import sys
from packaging.version import Version

print("true" if Version(sys.argv[1]) < Version(sys.argv[2]) else "false")
PY
) || {
echo "::error::Cannot compare package versions; leaving remote branches unchanged." >&2
exit 1
}
if [ "$older" = "true" ]; then
skip_update "Version $PACKAGE_VERSION is older than PR version $current_version; leaving it unchanged."
fi
}

queue_status() {
local pr="$1" status
if status=$(gh api "repos/PostHog/posthog/issues/${pr}/comments?per_page=100" \
--paginate --slurp \
--jq '[.[][] | select(.user.login == "trunk-io[bot]") | .body] | last // ""'); then
printf '%s' "$status" \
| grep -oE "Running tests on this pull request|waiting to start tests" | head -n 1 || true
else
echo "::warning::Could not read the queue status of PR #${pr}; treating it as queued." >&2
echo "unknown"
fi
}

EXISTING=$(gh pr list \
--repo PostHog/posthog \
--state open \
--search "\"${TITLE_PREFIX}\" in:title sort:created-desc" \
--limit 100 \
--json title,number,headRefName,headRepositoryOwner \
--jq ".[] | select(.title | startswith(\"${TITLE_PREFIX}\")) | select(.headRepositoryOwner.login == \"PostHog\") | [.number, .headRefName, .title] | @tsv" | head -n 1)
IFS=$'\t' read -r EXISTING_PR EXISTING_BRANCH EXISTING_TITLE <<< "$EXISTING"

# Updating a queued PR's branch restarts the merge queue's checks.
# Leave queued PRs alone, including when their status cannot be read.
QUEUED=""
if [ -n "$EXISTING_PR" ]; then
check_version "$EXISTING_TITLE"
QUEUED=$(queue_status "$EXISTING_PR")
fi

if [ -n "$EXISTING_BRANCH" ] && [ -z "$QUEUED" ]; then
BRANCH_NAME="$EXISTING_BRANCH"
elif [ -n "$QUEUED" ]; then
echo "::notice::PR #${EXISTING_PR} is in the merge queue; checking a separate branch instead."
BRANCH_NAME="${PACKAGE_NAME}-upgrade-${PACKAGE_VERSION}"
# A rerun can collide with an already queued version-specific PR.
if ! DESTINATIONS=$(gh pr list \
--repo PostHog/posthog \
--state open \
--head "$BRANCH_NAME" \
--limit 100 \
--json title,number,headRefName,isCrossRepository \
--jq '.[] | select(.isCrossRepository == false) | [.number, .headRefName, .title] | @tsv'); then
skip_update "Could not check the fallback branch for an existing PR; leaving it unchanged."
fi
while IFS=$'\t' read -r DESTINATION_PR DESTINATION_BRANCH DESTINATION_TITLE; do
[ -n "$DESTINATION_PR" ] || continue
[ "$DESTINATION_BRANCH" = "$BRANCH_NAME" ] || continue
check_version "$DESTINATION_TITLE"
if [ -n "$(queue_status "$DESTINATION_PR")" ]; then
skip_update "Fallback PR #${DESTINATION_PR} is queued or its status is unknown; leaving it unchanged."
fi
done <<< "$DESTINATIONS"
else
BRANCH_NAME="${PACKAGE_NAME}-upgrade"
fi

echo "skip=false" >> "$GITHUB_OUTPUT"
echo "branch_name=$BRANCH_NAME" >> "$GITHUB_OUTPUT"
echo "title=${TITLE_PREFIX}${PACKAGE_VERSION}" >> "$GITHUB_OUTPUT"
{
echo 'body<<EOF'
echo '## Changes'
echo
echo "PostHog Python SDK version $PACKAGE_VERSION has been released. This updates the \`posthoganalytics\` and \`posthog\` dependency declarations to the same SDK version."
echo
echo '[GitHub releases](https://github.com/PostHog/posthog-python/releases) β€’ [`posthoganalytics` on PyPI](https://pypi.org/project/posthoganalytics/#history) β€’ [`posthog` on PyPI](https://pypi.org/project/posthog/#history)'
echo 'EOF'
} >> "$GITHUB_OUTPUT"

- name: Create main repo pull request
if: steps.generate-pr-details.outputs.skip == 'false'
id: main-repo-pr
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1
env:
Expand All @@ -99,18 +204,14 @@ jobs:
token: ${{ steps.upgrader.outputs.token }}
# PostHog/posthog requires signed commits, so commit through the GitHub API.
sign-commits: true
commit-message: "chore(deps): update ${{ github.event.inputs.package_name }} to ${{ github.event.inputs.package_version }}"
branch: ${{ steps.generate-branch-name.outputs.branch_name }}
commit-message: ${{ steps.generate-pr-details.outputs.title }}
branch: ${{ steps.generate-pr-details.outputs.branch_name }}
delete-branch: true
title: "chore(deps): update ${{ github.event.inputs.package_name }} to ${{ github.event.inputs.package_version }}"
body: |
## Changes

PostHog Python SDK version ${{ github.event.inputs.package_version }} has been released. This updates the `posthoganalytics` and `posthog` dependency declarations to the same SDK version.

[GitHub releases](https://github.com/PostHog/posthog-python/releases) β€’ [`posthoganalytics` on PyPI](https://pypi.org/project/posthoganalytics/#history) β€’ [`posthog` on PyPI](https://pypi.org/project/posthog/#history)
title: ${{ steps.generate-pr-details.outputs.title }}
body: ${{ steps.generate-pr-details.outputs.body }}

- name: Output pull request result
if: steps.generate-pr-details.outputs.skip == 'false'
shell: bash
env:
PACKAGE_NAME: ${{ github.event.inputs.package_name }}
Expand All @@ -119,7 +220,21 @@ jobs:
run: |
echo "PostHog pull request for $PACKAGE_NAME version $PACKAGE_VERSION ready: $PR_URL"

- name: Update pull request metadata
if: steps.generate-pr-details.outputs.skip == 'false'
env:
GH_TOKEN: ${{ steps.upgrader.outputs.token }}
PR_BODY: ${{ steps.generate-pr-details.outputs.body }}
PR_NUMBER: ${{ steps.main-repo-pr.outputs.pull-request-number }}
PR_TITLE: ${{ steps.generate-pr-details.outputs.title }}
run: |
gh pr edit "$PR_NUMBER" \
--repo PostHog/posthog \
--title "$PR_TITLE" \
--body "$PR_BODY"

- name: Assign reviewers
if: steps.generate-pr-details.outputs.skip == 'false'
env:
GH_TOKEN: ${{ steps.upgrader.outputs.token }}
PR_NUMBER: ${{ steps.main-repo-pr.outputs.pull-request-number }}
Expand Down
18 changes: 18 additions & 0 deletions openfeature-provider/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,24 @@ All notable changes to `openfeature-provider-posthog` are documented here. This
file is maintained by [Sampo](https://github.com/bruits/sampo) from changesets in
`.sampo/changesets/` that target `pypi/openfeature-provider-posthog`.

## 0.1.100 β€” 2026-10-09

### Patch changes

- Updated dependencies: posthog@7.67.0

## 0.1.99 β€” 2026-10-08

### Patch changes

- Updated dependencies: posthog@7.66.0

## 0.1.98 β€” 2026-10-08

### Patch changes

- Updated dependencies: posthog@7.65.0

## 0.1.97 β€” 2026-10-06

### Patch changes
Expand Down
2 changes: 1 addition & 1 deletion openfeature-provider/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "openfeature-provider-posthog"
version = "0.1.97"
version = "0.1.100"
description = "Official PostHog provider for the OpenFeature Python SDK."
readme = "README.md"
authors = [{ name = "PostHog", email = "engineering@posthog.com" }]
Expand Down
22 changes: 22 additions & 0 deletions posthog/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,27 @@
# posthog

## 7.67.0 β€” 2026-10-09

### Minor changes

- [9baf8a5](https://github.com/posthog/posthog-python/commit/9baf8a5a84545568952490ff9397f599e0d6c9b4) Add a Django REST Framework error-only exception handler that captures handled 5xx API exceptions while preserving DRF responses, leaves expected 4xx errors excluded by default, and consistently inherits Django middleware, client, request-filter, and exception-autocapture configuration. β€” Thanks @hpouillot!

## 7.66.0 β€” 2026-10-08

### Minor changes

- [127009d](https://github.com/posthog/posthog-python/commit/127009dc394af6638a3ea8a88bdb0dcae46db0c3) Add `resolve_original_tool` for low-level MCP servers. Fresh server instances can now remove PostHog-owned arguments before strict tool validation.

Raw low-level servers also remove PostHog-owned arguments after `tools/list`. A tool-owned `context` remains tool data. β€” Thanks @gesh!
- [1c6a47a](https://github.com/posthog/posthog-python/commit/1c6a47ae235c127329c38db02cb6837a7f1c6434) Add framework-independent ASGI middleware for automatic request context and unhandled exception capture, supporting FastAPI, Starlette, Litestar, WebSockets, sync or async filters, additional request properties, and opt-in PostHog tracing headers without requiring a framework dependency. β€” Thanks @hpouillot!
- [292855d](https://github.com/posthog/posthog-python/commit/292855d1f267ad50e9c3b8341c9fbc839b36cf5c) Add a Flask integration that creates isolated request contexts, attaches safe request metadata and tracing identity, and automatically captures unhandled application exceptions without changing Flask's error behavior. β€” Thanks @hpouillot!

## 7.65.0 β€” 2026-10-08

### Minor changes

- [6d6cc3d](https://github.com/posthog/posthog-python/commit/6d6cc3dac989569f3c571308e4db9b9f6ecf7b36) Standardize exception capture metadata, including severity, capture source, typed integration metadata, mechanism semantics, bounded exception-group traversal, and deterministic cause linkage. Application overrides of reserved exception properties remain supported during a deprecation period, emit a warning, and will be removed in the next major version. β€” Thanks @hpouillot!

## 7.64.1 β€” 2026-10-06

### Patch changes
Expand Down
2 changes: 2 additions & 0 deletions posthog/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -795,6 +795,8 @@ def capture_exception(
exception: The exception to capture. If not provided, the current exception is captured via `sys.exc_info()`
**kwargs: Optional capture arguments including distinct_id, properties,
timestamp, uuid, groups, flags, send_feature_flags, disable_geoip, and options.
Overriding reserved exception properties through ``properties`` is
deprecated and will stop working in the next major version.

Details:
Capture exception is idempotent - if it is called twice with the same exception instance, only a occurrence will be tracked in posthog. This is because, generally, contexts will cause exceptions to be captured automatically. However, to ensure you track an exception, if you catch and do not re-raise it, capturing it manually is recommended, unless you are certain it will have crossed a context boundary (e.g. by existing a `with posthog.new_context():` block already). If the passed exception was raised and caught, the captured stack trace will consist of every frame between where the exception was raised and the point at which it is captured (the "traceback"). If the passed exception was never raised, e.g. if you call `posthog.capture_exception(ValueError("Some Error"))`, the stack trace captured will be the full stack trace at the moment the exception was captured. Note that heavy use of contexts will lead to truncated stack traces, as the exception will be captured by the context entered most recently, which may not be the point you catch the exception for the final time in your code. It's recommended to use contexts sparingly, for this reason. `capture_exception` takes the same set of optional arguments as `capture`.
Expand Down
61 changes: 57 additions & 4 deletions posthog/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@
_get_current_otel_span_properties,
handle_in_app,
mark_exception_as_captured,
_normalize_exception_level,
try_attach_code_variables_to_frames,
)
from posthog.feature_flag_evaluations import (
Expand Down Expand Up @@ -2209,7 +2210,9 @@ def capture_exception(
Args:
exception: The exception to capture.
distinct_id: The distinct ID of the user.
properties: A dictionary of additional properties.
properties: A dictionary of additional properties. Overriding reserved
exception properties is deprecated and will stop working in the next
major version.
flags: A ``FeatureFlagEvaluations`` snapshot from ``evaluate_flags()``.
Attaches those exact flag values to the captured `$exception` event.
send_feature_flags: Deprecated. Pass ``flags`` from ``evaluate_flags()`` instead.
Expand Down Expand Up @@ -2252,7 +2255,16 @@ def capture_exception(
return None

# Format stack trace for cymbal
all_exceptions_with_trace = exceptions_from_error_tuple(exc_info)
capture_metadata_input = dict(kwargs).get("_capture_metadata")
capture_metadata = (
capture_metadata_input
if isinstance(capture_metadata_input, dict)
else {}
)
mechanism = capture_metadata.get("mechanism")
all_exceptions_with_trace = exceptions_from_error_tuple(
exc_info, mechanism=mechanism if isinstance(mechanism, dict) else None
)

# Add in-app property to frames in the exceptions
event = handle_in_app(
Expand All @@ -2266,11 +2278,52 @@ def capture_exception(
)
all_exceptions_with_trace_and_in_app = event["exception"]["values"]

reserved_properties = {
"$exception_list",
"$exception_level",
"$exception_source",
"$debug_images",
"$exception_handled",
"$exception_types",
"$exception_values",
"$exception_sources",
"$exception_functions",
"$exception_fingerprint_version",
"$exception_fingerprint_record",
"$exception_issue_id",
"$exception_release",
"$cymbal_errors",
}
reserved_property_overrides = reserved_properties.intersection(properties)
if reserved_property_overrides:
try:
warnings.warn(
"Reserved exception properties passed through "
"`capture_exception(properties=...)` currently override "
"SDK-owned metadata, but this behavior is deprecated and will "
"be removed in the next major version: "
+ ", ".join(sorted(reserved_property_overrides)),
DeprecationWarning,
stacklevel=2,
)
except DeprecationWarning:
# capture_exception must not drop an event when applications
# promote deprecation warnings to errors.
pass

caller_properties = properties
properties = {
"$exception_list": all_exceptions_with_trace_and_in_app,
**_get_current_otel_span_properties(),
**properties,
"$exception_list": all_exceptions_with_trace_and_in_app,
"$exception_level": _normalize_exception_level(
capture_metadata.get("level")
)
or "error",
}
source = capture_metadata.get("source")
if isinstance(source, str) and source:
properties["$exception_source"] = source
properties.update(caller_properties)

context_enabled = get_capture_exception_code_variables_context()
context_mask = get_code_variables_mask_patterns_context()
Expand Down
Loading
Loading