PostHog / posthog-php

MIT License
18 stars 19 forks source link

Proposal: add optional cache for feature flags #43

Open drjayvee opened 1 year ago

drjayvee commented 1 year ago

I'm trialing PostHog for our SaaS app. For us, the lack of caching for feature flags are a total no-go. (We're currently canary-releasing a feature that requires a decide on every single page view.)

So I've added caching by extending the PostHog\Client and overriding fetchFeatureVariants.

<?php

use PostHog\Client;
use PostHog\HttpClient;
use Psr\Cache\CacheItemPoolInterface;

class CachingClient extends Client {

    private const DEFAULT_EXPIRY_DURATION = 'PT5M';    // five minutes

    private ?CacheItemPoolInterface $cache;
    private \DateInterval $cacheExpiryInterval;

    public function __construct(
        string $apiKey,
        array $options = [],
        ?HttpClient $httpClient = null,
        string $personalAPIKey = null,
        CacheItemPoolInterface $cache = null
    ) {
        parent::__construct($apiKey, $options, $httpClient, $personalAPIKey);

        $this->cache = $cache;
        $this->cacheExpiryInterval = new \DateInterval($options['cacheDuration'] ?? self::DEFAULT_EXPIRY_DURATION);
    }

    public function fetchFeatureVariants(string $distinctId, array $groups = [], array $personProperties = [], array $groupProperties = []): array {
        $callParent = fn() => parent::fetchFeatureVariants($distinctId, $groups, $personProperties, $groupProperties);

        if (!$this->cache) {
            return $callParent();
        }

        $query = substr(md5(json_encode([$groups, $personProperties, $groupProperties])), 0, 8);    // yes, md5 is totally suitable here: it's fast and provides a good distribution
        $cacheItem = $this->cache->getItem("FeatureFlags.dId=$distinctId.q=$query");

        if ($cacheItem->isHit()) {
            return $cacheItem->get();
        }

        $result = $callParent();

        $this->cache->save(
            $cacheItem
                ->expiresAfter($this->cacheExpiryInterval)
                ->set($result)
        );

        return $result;
    }

}

I'd be happy to create a PR to add this to the base class.