feat(flags): add official PostHog OpenFeature provider#695
Conversation
|
Reviews (1): Last reviewed commit: "Add official PostHog OpenFeature provide..." | Re-trigger Greptile |
posthog-python Compliance ReportDate: 2026-06-30 21:00:01 UTC ✅ All Tests Passed!45/45 tests passed Capture Tests✅ 29/29 tests passed View Details
Feature_Flags Tests✅ 16/16 tests passed View Details
|
|
Tested this locally end-to-end against a running PostHog instance (not just the mocked unit/e2e tests) — registered the provider with the OpenFeature SDK and evaluated a real boolean flag through Repro script I used: """
Example demonstrating how to use the PostHog OpenFeature provider.
This example shows:
1. Initializing the PostHog client
2. Registering the PostHogProvider with the OpenFeature SDK
3. Evaluating a boolean feature flag via the OpenFeature API
"""
import os
import posthog
from openfeature import api
from openfeature.evaluation_context import EvaluationContext
from openfeature.contrib.provider.posthog import PostHogProvider
# Initialize the PostHog client
posthog_client = posthog.Posthog(
project_api_key=os.getenv("POSTHOG_API_KEY", "phc_PIPWvLdxL4N9RUvpyENExMOFEz2jXuk5ehmyXFG3A2k"),
host=os.getenv("POSTHOG_HOST", "http://localhost:8010"),
)
# Register the PostHog provider with OpenFeature
provider = PostHogProvider(posthog_client)
api.set_provider(provider)
# Get the OpenFeature client
client = api.get_client()
# The flag key to evaluate
flag_key = os.getenv("POSTHOG_FLAG_KEY", "my-flag")
# Evaluation context: targeting_key maps to PostHog's distinct_id
ctx = EvaluationContext(targeting_key="test-user")
# Evaluate the boolean flag
result = client.get_boolean_details(flag_key, False, ctx)
print(f"Flag: {flag_key}")
print(f"Value: {result.value}")
print(f"Reason: {result.reason}")
if result.error_message:
print(f"Error: {result.error_message}")
posthog_client.shutdown()(The |
|
Reviews (2): Last reviewed commit: "Address review: add missing tests and op..." | Re-trigger Greptile |
haacked
left a comment
There was a problem hiding this comment.
Clean, well-tested provider, nothing blocking. A few non-blocking suggestions inline, the correctness one (TYPE_MISMATCH on unmatched experiments) is the one worth a decision.
I'll leave approval to the client libraries team who may want a look.
| evaluation_context: Optional[EvaluationContext] = None, | ||
| ) -> FlagResolutionDetails[str]: | ||
| result = self._resolve(flag_key, evaluation_context) | ||
| if result.variant is None: |
There was a problem hiding this comment.
suggestion: Unenrolled users get TYPE_MISMATCH errors when they shouldn't. When a user matches no condition on a multivariate flag, PostHog returns enabled=False, variant=None, so the result.variant is None check here raises TypeMismatchError. The OpenFeature SDK then returns the correct default value but marks it with error_code=TYPE_MISMATCH and reason=ERROR, so normal non-enrollment gets flagged as type-mismatch errors.
You can tell the two cases apart by enabled: when enabled=True, variant=None, it's a genuine mismatch (boolean flag read as string), while enabled=False, variant=None means the flag is off or nobody matched. Return the default with a non-error reason in that case:
result = self._resolve(flag_key, evaluation_context)
if result.variant is None:
if not result.enabled:
return FlagResolutionDetails(
value=default_value,
reason=self._map_reason(result),
flag_metadata=self._flag_metadata(result),
)
raise TypeMismatchError(
f"Flag '{flag_key}' has no string variant (boolean flag)."
)The same variant is None branch is in _resolve_number; keep the non-numeric-variant case there raising TypeMismatchError. One tradeoff: a disabled boolean flag read as a string would reclassify from TYPE_MISMATCH to DISABLED, since enabled=False, variant=None can't tell the two apart.
There was a problem hiding this comment.
Fixed in 8f24613. enabled=False, variant=None now returns the default with a normal reason (DEFAULT/DISABLED) instead of raising TypeMismatchError; the genuine boolean-read-as-string case (enabled=True, variant=None) still raises. Applied the same in _resolve_number, and (per Manoel) in resolve_object_details. Added detail tests asserting error_code is None on the non-enrollment path.
| groups = attrs.get(GROUPS_KEY) or {} | ||
| group_properties = attrs.get(GROUP_PROPERTIES_KEY) or {} | ||
| person_properties = {k: v for k, v in attrs.items() if k not in _RESERVED_KEYS} | ||
| groups = groups if isinstance(groups, dict) else {} |
There was a problem hiding this comment.
suggestion: Nothing tests the non-dict fallback here. test_context_split only passes well-formed dicts, so when a caller sets groups to a string or list, the isinstance(groups, dict) guard that coerces it to {} is unverified. Drop that guard in a later refactor and a non-dict would flow straight into get_feature_flag_result with no test to catch it.
Add a case mirroring test_context_split that passes groups="acme" and asserts the forwarded kwargs["groups"] is None.
There was a problem hiding this comment.
Fixed in 8f24613. Added test_context_split_non_dict_groups_coerced_to_none, parametrized over "acme", a list, and an int, asserting the forwarded groups/group_properties kwargs are None.
| - name: Set up Python 3.12 | ||
| uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 | ||
| with: | ||
| python-version: 3.12 |
There was a problem hiding this comment.
suggestion: The package declares support for Python 3.10 through 3.14 (requires-python plus classifiers in pyproject.toml), but this job only tests on 3.12. mypy targets 3.10, so type errors are caught, but nothing exercises the floor or ceiling at runtime. A 3.10-only incompatibility or a 3.14 behavior change would ship unverified.
The tests and import-check jobs already matrix across all five versions.
strategy:
matrix:
python-version: ['3.10', '3.11', '3.12', '3.13', '3.14']
steps:
...
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55
with:
python-version: ${{ matrix.python-version }}There was a problem hiding this comment.
Agree the matrix should mirror the tests/import-check jobs (3.10–3.14). I do not edit .github/workflows/** in this automated review-fix turn (guardrail), so I have left this for a maintainer to apply directly.
| evaluation_context: Optional[EvaluationContext] = None, | ||
| ) -> FlagResolutionDetails[bool]: | ||
| result = self._resolve(flag_key, evaluation_context) | ||
| return FlagResolutionDetails( |
There was a problem hiding this comment.
nit: The four typed resolvers end with the same FlagResolutionDetails tail except for value. A small helper keeps the reason/flag_metadata wiring in one place:
def _details(self, value: _T, result: FeatureFlagResult) -> FlagResolutionDetails[_T]:
return FlagResolutionDetails(
value=value,
variant=result.variant,
reason=self._map_reason(result),
flag_metadata=self._flag_metadata(result),
)There was a problem hiding this comment.
Done in 8f24613 — extracted _details(value, result) and routed all four typed resolvers (and the default-return paths) through it.
| @@ -0,0 +1,103 @@ | |||
| # PostHog provider for OpenFeature (Python) | |||
There was a problem hiding this comment.
i'd move all install/snippets to https://posthog.com/docs/feature-flags and just point to the docs
single source of truth for docs, readme gets outdated
There was a problem hiding this comment.
Done in 8f24613 — slimmed the README to a minimal quickstart and pointed to https://posthog.com/docs/feature-flags as the single source of truth. Kept only the OpenFeature-specific registration snippet + context mapping (not yet on the docs site); those can move there once the provider is documented.
There was a problem hiding this comment.
Done earlier in 8f24613 — README slimmed to a minimal quickstart pointing at https://posthog.com/docs/feature-flags as the source of truth.
|
since its a new distribution, you'd need to adapt https://github.com/PostHog/posthog-python/blob/main/.github/workflows/release.yml to publish multiple packages |
|
|
||
| [project] | ||
| name = "openfeature-provider-posthog" | ||
| version = "0.1.0" |
There was a problem hiding this comment.
needs to set up sampo here, see other comment related to the release process
There was a problem hiding this comment.
The release wiring (Sampo for the new package + adapting release.yml to publish a second distribution) is a .github/workflows/** + release-config change I will not make in this automated turn. Flagged for a maintainer in a summary comment on the PR.
There was a problem hiding this comment.
Implemented in 5236b4b. Sampo already auto-discovers the package as pypi/openfeature-provider-posthog (it has its own pyproject with name+version), and release.yml now builds/publishes/tags it — gated so it only fires when a changeset bumps the provider, leaving posthog-only releases untouched. Added openfeature-provider/CHANGELOG.md for Sampo to maintain. First publish still needs a one-time PyPI trusted-publisher registration for the new project.
There was a problem hiding this comment.
Correction to my earlier reply — you were right, Sampo does not auto-discover this package. I checked sampo-core's pip adapter: it only discovers packages from the root pyproject's [tool.uv.workspace] members (plus the root). Fixed properly in b40251e by declaring a uv workspace (members = ["openfeature-provider"]) so sampo add -p pypi/openfeature-provider-posthog works and sampo release bumps the provider. Details in the PR comment.
|
Optional nice-to-have: add a CI smoke test that installs the built wheel into a fresh env and imports openfeature.contrib.provider.posthog, since namespace packaging issues are easy to miss with source-tree tests. |
|
The unmatched-user/default behavior should include object flags as well? |
|
left a bunch of comments, main blocker is the release process and a few other comments/suggestions, Phil added good points as well |
Adds `@posthog/openfeature-provider`, a JS port of the Python OpenFeature provider (PostHog/posthog-python#695). OpenFeature ships two SDKs with incompatible Provider contracts, so this package ships one provider for each, sharing a runtime-agnostic mapping core: - `/server` — `PostHogServerProvider` wraps `posthog-node` against `@openfeature/server-sdk` (async, multi-user, distinct id from `targetingKey`). - `/web` — `PostHogWebProvider` wraps `posthog-js` against `@openfeature/web-sdk` (synchronous, single-user, context reconciled via `onContextChange`). Both resolve through `getFeatureFlagResult`: boolean→enabled, string→variant, number→parsed variant, object→payload; missing flag→FLAG_NOT_FOUND, wrong type→TYPE_MISMATCH. The SDK/client deps are optional peers so a node-only or browser-only app never installs the other paradigm's stack. Generated-By: PostHog Code Task-Id: 46a3f8c9-fbcd-460e-9457-fca583955e5a
|
@marandaneto — implemented your remaining suggestions in
One manual prerequisite for the first provider release (can't be done in a PR): register a PyPI trusted publisher for Verified locally: 37 tests, ruff, mypy, |
|
Still not fixed / still worth commenting:
those are still open, the 2. is optional but i think its cool to test across all major versions like we do for the main package |
| # Publish the `openfeature-provider-posthog` distribution, but only when | ||
| # this release actually bumped it (a changeset targeting | ||
| # `pypi/openfeature-provider-posthog` was processed by Sampo, changing its | ||
| # version in openfeature-provider/pyproject.toml). Releases that only touch | ||
| # `posthog` skip these steps entirely, so the core release is unaffected. | ||
| # These run after the posthog publish/tag/release above so a provider issue | ||
| # can never block the main release. | ||
| # | ||
| # NOTE: the first provider release requires a PyPI trusted publisher to be | ||
| # registered for `openfeature-provider-posthog` (this workflow, environment | ||
| # "Release"), exactly like posthog/posthoganalytics. | ||
| - name: Detect openfeature-provider release | ||
| id: of-provider | ||
| if: steps.commit-release.outputs.commit-hash != '' | ||
| run: | | ||
| if git diff --name-only HEAD~1 HEAD -- openfeature-provider/pyproject.toml | grep -q .; then | ||
| version=$(python3 -c "import tomllib; print(tomllib.load(open('openfeature-provider/pyproject.toml','rb'))['project']['version'])") | ||
| echo "released=true" >> "$GITHUB_OUTPUT" | ||
| echo "version=$version" >> "$GITHUB_OUTPUT" | ||
| echo "openfeature-provider-posthog bumped to $version; will publish." | ||
| else | ||
| echo "released=false" >> "$GITHUB_OUTPUT" | ||
| echo "openfeature-provider-posthog not changed in this release; skipping." | ||
| fi | ||
|
|
||
| - name: Build openfeature-provider-posthog | ||
| if: steps.of-provider.outputs.released == 'true' | ||
| working-directory: openfeature-provider | ||
| run: uv build --package openfeature-provider-posthog --out-dir dist | ||
|
|
||
| - name: Publish openfeature-provider-posthog to PyPI | ||
| if: steps.of-provider.outputs.released == 'true' | ||
| uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0 | ||
| with: | ||
| packages-dir: openfeature-provider/dist | ||
|
|
||
| - name: Tag openfeature-provider release | ||
| if: steps.of-provider.outputs.released == 'true' | ||
| env: | ||
| GH_TOKEN: ${{ steps.releaser.outputs.token }} | ||
| PROVIDER_VERSION: ${{ steps.of-provider.outputs.version }} | ||
| COMMIT_HASH: ${{ steps.commit-release.outputs.commit-hash }} | ||
| run: | | ||
| gh api "repos/${{ github.repository }}/git/refs" \ | ||
| -f "ref=refs/tags/openfeature-provider-posthog-v${PROVIDER_VERSION}" \ | ||
| -f "sha=${COMMIT_HASH}" |
There was a problem hiding this comment.
i think a build matrix is better as suggested before
for example https://github.com/PostHog/posthog-ruby/blob/4579a7f2c1bb253bf67cb560f14a72190118ff4f/.github/workflows/release.yml#L222-L233
those are also N packages
| ## Development | ||
|
|
||
| ```bash | ||
| cd openfeature-provider | ||
| uv sync --extra dev | ||
| uv run pytest | ||
| uv run ruff check . | ||
| uv run mypy . | ||
| ``` |
There was a problem hiding this comment.
move to https://github.com/PostHog/posthog-python/blob/main/CONTRIBUTING.md or create its own CONTRIBUTING
| ## Install | ||
|
|
||
| ```bash | ||
| pip install openfeature-provider-posthog | ||
| ``` | ||
|
|
||
| ## Quickstart | ||
|
|
||
| ```python | ||
| import posthog | ||
| from openfeature import api | ||
| from openfeature.evaluation_context import EvaluationContext | ||
| from openfeature.contrib.provider.posthog import PostHogProvider | ||
|
|
||
| client = posthog.Posthog("phc_project_api_key", host="https://us.i.posthog.com") | ||
| api.set_provider(PostHogProvider(client, default_distinct_id="anonymous")) | ||
|
|
||
| of_client = api.get_client() | ||
| ctx = EvaluationContext(targeting_key="user-123", attributes={"plan": "pro"}) | ||
| enabled = of_client.get_boolean_value("my-flag", False, ctx) | ||
| ``` | ||
|
|
||
| The OpenFeature `targeting_key` maps to PostHog's `distinct_id`; other context | ||
| attributes become person properties, with reserved keys `groups` and | ||
| `group_properties` mapping to PostHog groups. You own the `Posthog` client | ||
| lifecycle — call `client.shutdown()` when done. |
There was a problem hiding this comment.
this gets outdated super fast, lets point to our docs only, you can link to the specific open feature section instead
Addresses review feedback from @haacked and @marandaneto. - Correctness (haacked + marandaneto): a user who matches no condition or a disabled flag (enabled=False, with no variant/object payload) is no longer reported as a TYPE_MISMATCH error. The string/integer/float/object resolvers now return the caller's default with a normal reason (DEFAULT/DISABLED) in that case, and only raise TypeMismatchError for a genuine mismatch (enabled=True but the value can't be coerced to the requested type). Applied consistently across string, number, and object. - Refactor (haacked): extract a shared `_details()` helper for the repeated reason/flag_metadata wiring across the typed resolvers. - Tests (haacked): add non-dict `groups`/`group_properties` coercion cases asserting they are forwarded as None; add detail tests asserting unmatched string/number/object resolution returns the default with no error_code. - Docs (marandaneto): slim the README to a minimal quickstart and point to https://posthog.com/docs/feature-flags as the single source of truth. Generated-By: PostHog Code Task-Id: 392fb0da-49bb-4c96-96c7-1b39b0348d32
Implements the remaining review suggestions from @marandaneto. - Release (release.yml): publish `openfeature-provider-posthog` as a second distribution. Sampo already auto-discovers the package (it has its own pyproject), so the new steps only build/publish/tag the provider when this release actually bumped its version (a changeset targeting `pypi/openfeature-provider-posthog`). posthog-only releases skip them, and they run after the posthog publish/tag/release so they can never block the core release. Uses the same PyPI OIDC trusted-publishing action as posthog/posthoganalytics (a trusted publisher for the new project must be registered before the first provider release). - Sampo: add openfeature-provider/CHANGELOG.md for Sampo to maintain. - CI (ci.yml): add a clean-env smoke test that installs the built wheel and imports openfeature.contrib.provider.posthog, catching namespace-packaging regressions that source-tree tests miss. - pyproject: pin posthog to the tested major (>=7.0.0,<8.0.0); the local-branch build via [tool.uv.sources] was already in place. Generated-By: PostHog Code Task-Id: 392fb0da-49bb-4c96-96c7-1b39b0348d32
Fixes the release/versioning gap @marandaneto found: `sampo add -p pypi/openfeature-provider-posthog` failed with "not found in the workspace". Sampo's PyPI adapter only discovers packages listed in the root pyproject's [tool.uv.workspace] members (plus the root) — there is no auto-discovery — so the provider was invisible to Sampo and the release detection never triggered. - Root pyproject: declare a uv workspace with `members = ["openfeature-provider"]`. Sampo now discovers `pypi/openfeature-provider-posthog`, so `sampo add` works and `sampo release` bumps openfeature-provider/pyproject.toml from a changeset. The root uv.lock change is purely additive (only the member + openfeature-sdk; no churn to existing posthog pins), and the main `posthog` build is unaffected (explicit packages list). - Provider pyproject: resolve posthog via `{ workspace = true }` instead of a path source; drop the now-redundant standalone uv.lock (the workspace shares the root lock). - CI: the provider job now matrixes Python 3.10–3.14 (matching the main package, per @marandaneto/@haacked) and uses workspace-aware commands (`--package openfeature-provider-posthog`, `uv build --out-dir dist`). - release.yml: build the provider with `--package ... --out-dir dist` so its artifacts stay isolated from the posthog dist. Verified locally: root posthog sync + import, provider sync/tests (37)/ruff/mypy/ build/twine/clean-env-import across the workspace, and the django5 integration project (not a member) still syncs standalone. Sampo itself couldn't be run here (no cargo), but the config matches exactly what its pip adapter parses. Generated-By: PostHog Code Task-Id: 392fb0da-49bb-4c96-96c7-1b39b0348d32
Addresses further review from @marandaneto. - release.yml: restructure into the canonical PostHog multi-package shape (cf. posthog-ruby). A single approval-gated `version-bump` job (environment "Release") bumps versions, commits, and regenerates references; a separate `publish` job with no environment runs a `package` matrix (fail-fast, max-parallel 1) over posthog, posthoganalytics, and openfeature-provider-posthog. Each entry detects whether its version changed in the release commit and only then builds/publishes/tags. Keeping the approval on version-bump (not the matrix) preserves a single approval per release. NOTE: the no-environment publish job means each package's PyPI trusted publisher must point at the `publish` job (not an environment) — a one-time PyPI-side config matraneto/maintainers control. - README: drop the install/quickstart snippets that drift; point to https://posthog.com/docs/feature-flags (OpenFeature section) as the single source of truth. - Move the development instructions out of the README into a dedicated openfeature-provider/CONTRIBUTING.md with the workspace-aware commands. Generated-By: PostHog Code Task-Id: 392fb0da-49bb-4c96-96c7-1b39b0348d32
47016c2 to
9dfca3c
Compare
Per review: don't keep a separate provider README that drifts — point to the PostHog docs (single source of truth) instead. Companion docs page is added in PostHog/posthog.com#18006. - Delete openfeature-provider/README.md. - pyproject: drop the `readme` field (it referenced the deleted file) and add a Documentation URL so the PyPI page links straight to the docs. - Root README: the OpenFeature entry now links to https://posthog.com/docs/feature-flags/installation/openfeature instead of the in-repo provider README. uv build + twine check still pass (only a non-fatal missing-long_description warning, expected without a README). Generated-By: PostHog Code Task-Id: 392fb0da-49bb-4c96-96c7-1b39b0348d32
Follow-up to dropping the provider README (these were left unstaged in the prior commit, which deleted the README but not its references): - openfeature-provider/pyproject.toml: drop the `readme = "README.md"` field (it pointed at the now-deleted file and would break the build) and add a Documentation URL to [project.urls] so PyPI links straight to the docs. - README.md: the OpenFeature entry links to https://posthog.com/docs/feature-flags/installation/openfeature. Generated-By: PostHog Code Task-Id: 392fb0da-49bb-4c96-96c7-1b39b0348d32
Review feedback — all addressed ✅Thanks @marandaneto and @haacked (and Greptile) for the thorough review. Summary of what changed and how. Greptile
@haacked
@marandaneto
Status
One follow-up for maintainers (not blocking this PR)The |
| version_file: pyproject.toml | ||
| build: uv run make build_release | ||
| packages_dir: dist | ||
| tag_prefix: "v" |
There was a problem hiding this comment.
| tag_prefix: "v" | |
| tag_prefix: "posthog-v" |
There was a problem hiding this comment.
i think it makes sense like this now
| build: uv run make build_release | ||
| packages_dir: dist | ||
| tag_prefix: "v" | ||
| changelog: CHANGELOG.md |
There was a problem hiding this comment.
i think we should move this to posthog/CHANGELOG.md now
and the root changelog points to the inner changelogs
similar to https://github.com/PostHog/posthog-ruby/blob/main/CHANGELOG.md
| - name: Create ${{ matrix.package.name }} GitHub Release | ||
| if: steps.detect.outputs.has-new-version == 'true' && matrix.package.github_release | ||
| env: | ||
| GH_TOKEN: ${{ steps.releaser.outputs.token }} | ||
| TAG: ${{ matrix.package.tag_prefix }}${{ steps.detect.outputs.version }} | ||
| CHANGELOG_FILE: ${{ matrix.package.changelog }} | ||
| run: | | ||
| CHANGELOG_ENTRY=$(awk -v defText="see ${CHANGELOG_FILE}" '/^## /{if (flag) exit; flag=1; next} flag; END{if (!flag) print defText}' "${CHANGELOG_FILE}" | sed '/[^[:space:]]/,$!d' | tac | sed '/[^[:space:]]/,$!d' | tac) | ||
| gh release create "$TAG" --notes "$CHANGELOG_ENTRY" |
| @@ -0,0 +1,5 @@ | |||
| # Changelog | |||
There was a problem hiding this comment.
| # Changelog | |
| # openfeature-provider-posthog |
make sure the https://github.com/PostHog/posthog-python/pull/695/changes#r3498456802 works here since its fragile
marandaneto
left a comment
There was a problem hiding this comment.
left a few comments still, but approving to unblock
|
probably requires setting up pypi for the new package (trusted publisher) |
Addresses @marandaneto's 2026-06-30 review comments: - openfeature-provider/CHANGELOG.md: title is now `# openfeature-provider-posthog` (was `# Changelog`), so Sampo's `## <version>` sections sit under a package-named heading. - release.yml: make the GitHub release-notes extraction robust (the previous awk | sed | tac | sed | tac pipeline was fragile). It now falls back to a "See <CHANGELOG>" pointer when the changelog is missing or has no section yet (e.g. a package's first release), so a release is never blocked on notes. Verified against the provider changelog (no section -> fallback), the root posthog changelog (extracts the latest section), and a missing file. Not changed, with rationale: - posthog tag prefix kept as `v` (not `posthog-v`): the Sampo config sets `short_tags = "posthog"` which intentionally tags the posthog package as `v{version}`, matching existing `vX.Y.Z` tags. Switching to `posthog-v` would diverge from both. - Did not move the root CHANGELOG.md to posthog/CHANGELOG.md: Sampo writes a package's changelog in its manifest directory, and the posthog package's manifest is the repo root, so root CHANGELOG.md *is* the posthog changelog. The posthog-ruby layout works because its gem lives in a posthog-ruby/ subdir; replicating it here would require relocating the entire posthog package. Generated-By: PostHog Code Task-Id: 392fb0da-49bb-4c96-96c7-1b39b0348d32
…age repos Per @marandaneto's review and matching posthog-ruby's multi-package convention. posthog-ruby tags every package with its name prefix once it went multi-package (`posthog-ruby-v3.15.1`, `posthog-rails-v3.15.0`) and carries no `short_tags` in its Sampo config. This mirrors that: - release.yml: posthog package now tags as `posthog-v{version}` (was `v{version}`), so all published packages are consistently name-prefixed (`posthog-v…`, `openfeature-provider-posthog-v…`). posthoganalytics stays untagged (it is a same-version mirror of posthog). - .sampo/config.toml: drop `short_tags = "posthog"`. Tags are created manually in release.yml, so this was inert, but it implied `v{version}` for posthog and was inconsistent with the new prefix; removing it aligns Sampo's default tag format with the workflow and with posthog-ruby. Generated-By: PostHog Code Task-Id: 392fb0da-49bb-4c96-96c7-1b39b0348d32
Queues the provider's first release. Sets the manifest to 0.0.0 (the "nothing released yet" state) and adds a minor changeset so Sampo's release bumps it to 0.1.0 — the intended first published version. On merge to main this triggers the release workflow, which (per the per-package detect gate) publishes ONLY openfeature-provider-posthog 0.1.0 and tags it openfeature-provider-posthog-v0.1.0; posthog/posthoganalytics are untouched because the root pyproject doesn't change in this release. The provider's PyPI pending trusted publisher (no environment) is already registered. Generated-By: PostHog Code Task-Id: 392fb0da-49bb-4c96-96c7-1b39b0348d32
Lets you validate the release end-to-end before merging, without touching prod. Adds a `dry-run-testpypi` job to release.yml gated on `github.event_name == 'workflow_dispatch' && github.ref != 'refs/heads/main'`. When the workflow is manually dispatched on a non-main branch, it runs `sampo release` (version bump only, no commit), builds openfeature-provider-posthog, and publishes it to TestPyPI via OIDC — exercising the Sampo workspace bump and the trusted-publisher handshake. It never touches main, prod PyPI, git tags, or GitHub releases. Because release.yml already lives on main with workflow_dispatch, dispatching it against this branch runs the branch's copy (incl. this job) — so no merge is needed to test. On real push-to-main releases the job is skipped (ref == main), so the production flow is unaffected. Requires a TestPyPI trusted publisher for openfeature-provider-posthog (workflow release.yml, no environment). Generated-By: PostHog Code Task-Id: 392fb0da-49bb-4c96-96c7-1b39b0348d32
|
Reviews (3): Last reviewed commit: "Merge branch 'main' into posthog-code/op..." | Re-trigger Greptile |
What
Adds an official PostHog provider for the OpenFeature Python SDK, built in this repo and shipped as a separate distribution (
openfeature-provider-posthog) under the OpenFeature namespace packageopenfeature.contrib.provider.posthog.It wraps a configured
posthog.Posthogclient and resolves all five OpenFeature flag types via the modern, non-deprecatedget_feature_flag_result(one call → value + variant + payload + reason):get_boolean_valueenabledget_string_valueget_integer_value/get_float_valueget_object_valueEvaluation context maps
targeting_key→distinct_id, reserved attributesgroups/group_properties→ PostHog groups, and every other attribute →person_properties.Behavior decisions (recommended defaults, easy to change)
targeting_key→TargetingKeyMissingError(OpenFeature-idiomatic; SDK returns the caller's default). Opt into anonymous eval viadefault_distinct_id="anonymous".get_string_valueon a boolean flag, non-numeric variant for int, non-object payload) →TypeMismatchError→ default returned, per spec.send_feature_flag_events=Trueby default (keeps$feature_flag_called/ experiments working); toggleable.posthog>=6.0.0floor.Layout
Repo wiring
openfeature-providerjob mirroringdjango5-integration—uv sync, pytest, ruff format/check, mypy, thenuv build+twine checkin the sub-project's own env.mypy.ini: excludesopenfeature-provider/.*from the root mypy pass (the namespace tree is type-checked in its own env whereopenfeature-sdkis installed).The new top-level
openfeature/tree is isolated from theposthogwheel (explicit packages list) and from theposthog.*-scoped public-API snapshot — verified.Verification
uv build+twine checkpass; wheel ships only the leaf package +py.typed, no clobberingopenfeature/__init__.py, no testsopenfeature/entries;public_api_checksnapshot up to dateFollow-ups (not in this PR)
openfeature-provider-posthogbefore first publish. A third-partyposthog-openfeature-provider-python(MPL-2.0, deprecated API, no object support) already exists on PyPI; this one is official, MIT, and uses the modern API with full object/JSON support.posthog/posthoganalytics; this dist needs its own version/tag + PyPI publish job. Noposthoganalyticstwin needed.Docs
Documentation (install + usage snippets) lives in the PostHog docs, not in the package README (kept minimal so it doesn't drift). Companion docs PR: PostHog/posthog.com#18006 — adds
/docs/feature-flags/installation/openfeature.🤖 Generated with Claude Code