* @copyright Since 2007 PrestaShop SA and Contributors * @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0) */ declare(strict_types=1); namespace PrestaShop\Module\PsxDesign\Tracker; if (!defined('_PS_VERSION_')) { exit; } use Context; use Exception; use Language; use PrestaShop\Module\PsxDesign\Account\Provider\PsAccountDataProvider; use PrestaShop\Module\PsxDesign\Account\Provider\TokenDecoder; use PrestaShop\Module\PsxDesign\Exception\PsxDesignAccountsException; use PrestaShop\Module\PsxDesign\Exception\PsxDesignApiException; use PrestaShop\Module\PsxDesign\Exception\PsxDesignTokenDecoderException; use Ramsey\Uuid\Uuid; use Segment; use Symfony\Component\HttpFoundation\ParameterBag; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\ServerBag; class SegmentTracker implements TrackerInterface { private const COOKIE_ANONYMOUS_ID = 'psxdesign_anonymous_id'; /** * @var PsAccountDataProvider */ private $accountDataProvider; /** * @var Context */ private $context; /** * @var Language */ private $language; /** * @var TokenDecoder */ private $decoder; /** * @var string */ private $segmentKey; /** * @var string */ private $moduleVersion; /** * @var string */ private $moduleName; /** * @var string */ private $currentTheme; /** * @var string */ private $event = ''; /** * @var string|null */ private static $anonymousId; /** * @var array */ private $properties = []; public function __construct( PsAccountDataProvider $accountDataProvider, Context $context, TokenDecoder $decoder, string $segmentKey, string $moduleVersion, string $moduleName, string $currentTheme ) { $this->accountDataProvider = $accountDataProvider; $this->context = $context; $this->language = $this->context->language; $this->segmentKey = $segmentKey; $this->decoder = $decoder; $this->moduleVersion = $moduleVersion; $this->moduleName = $moduleName; $this->currentTheme = $currentTheme; $this->initSegment(); $this->initAnonymousId(); } /** * Track event on segment * * @param string $event * @param array $properties * @param ServerBag|null $serverBag * * @return void */ public function track(string $event, array $properties = [], ServerBag $serverBag = null): void { $this->setProperties($properties); $this->setEvent($event); if (!$serverBag) { $serverBag = $this->getServerBag(); } $message = $this->buildMessage($this->getUserId(), $serverBag); $this->segmentTrack($message); } /** * @param array $message * * @return void */ public function segmentTrack(array $message): void { try { Segment::track($message); Segment::flush(); } catch (Exception $e) { throw new PsxDesignApiException('Failed to send data to segment', PsxDesignApiException::FAILED_TO_SEND_DATA_TO_SEGMENT); } } /** * Init segment client with the api key */ private function initSegment(): void { Segment::init($this->segmentKey); } /** * @param string|null $userId * @param ServerBag $serverBag * * @return array */ public function buildMessage(?string $userId, ServerBag $serverBag): array { $userAgent = $serverBag->get('HTTP_USER_AGENT'); $referer = $serverBag->get('HTTP_REFERER'); $httpHost = $serverBag->get('HTTP_HOST'); $requestUri = $serverBag->get('REQUEST_URI'); $url = ($serverBag->get('HTTPS') !== null && $serverBag->get('HTTPS') === 'on' ? 'https' : 'http') . "://$httpHost$requestUri"; return [ 'userId' => $userId, 'anonymousId' => $this->getAnonymousId(), 'event' => $this->getEvent(), 'channel' => 'browser', 'context' => [ 'userAgent' => $userAgent, 'locale' => $this->language->iso_code, 'page' => [ 'referrer' => $referer, 'url' => $url, ], ], 'properties' => array_merge([ 'module' => $this->moduleName, 'module_version' => $this->moduleVersion, ], $this->getProperties()), ]; } /** * @return string */ public function getEvent(): string { return $this->event; } /** * @param string $event */ public function setEvent(string $event): void { $this->event = $event; } /** * @return array */ public function getProperties(): array { return $this->properties; } /** * @param array $properties */ public function setProperties(array $properties): void { if (!isset($properties['theme_name'])) { $properties['theme_name'] = $this->currentTheme; } $this->properties = $properties; } /** * @return string */ public function getAnonymousId(): string { $this->initAnonymousId(); return self::$anonymousId; } /** * Returns server bag if not provided * * @return ServerBag */ private function getServerBag(): ServerBag { return Request::createFromGlobals()->server; } /** * Returns Cookies * * @return ParameterBag */ private function getCookies(): ParameterBag { return Request::createFromGlobals()->cookies; } /** * Initialize anonymous id which is generated in front end. * * @return void */ private function initAnonymousId(): void { try { $allCookies = $this->getCookies(); $cookie = $allCookies->get(self::COOKIE_ANONYMOUS_ID); } catch (Exception $e) { $cookie = $this->context->cookie->{self::COOKIE_ANONYMOUS_ID} ?? null; } if (!isset(self::$anonymousId)) { if ($cookie) { self::$anonymousId = $cookie; } else { $this->generateAnonymousId(); } } } /** * Cookie generation in front end is asynchronously, so could be that cookie still does not exist * we need to wait and check again in case cookie still do not exist we generate new one. * * @return void */ private function generateAnonymousId(): void { sleep(3); $cookie = $this->context->cookie->{self::COOKIE_ANONYMOUS_ID} ?? null; if (!$cookie) { self::$anonymousId = Uuid::uuid4()->toString(); /* we ignore next line cause it's not an error, we just follow the PS dev doc => https://devdocs.prestashop-project.org/8/development/components/cookie/ */ /* @phpstan-ignore-next-line */ $this->context->cookie->{self::COOKIE_ANONYMOUS_ID} = self::$anonymousId; } else { self::$anonymousId = $cookie; } } /** * @return string|null */ private function getUserId(): ?string { $userId = null; try { $accessToken = $this->accountDataProvider->getOrRefreshAccessToken(); if ($accessToken) { $userId = $this->decoder->decode($accessToken)->getUserId(); } } catch (PsxDesignTokenDecoderException|PsxDesignAccountsException $e) { return null; } return $userId; } }