PluginProbe
WPVR – 360 Panorama viewer and Virtual Tour Builder for WordPress / 8.5.37
WPVR – 360 Panorama viewer and Virtual Tour Builder for WordPress v8.5.37
9.1.3 9.1.2 9.1.1 9.1.0 9.0.3 9.0.2 9.0.1 9.0.0 8.5.79 8.5.78 8.5.77 8.5.76 8.5.75 8.5.74 8.5.73 8.5.72 8.5.71 8.5.70 8.5.69 8.5.68 8.5.35 8.5.36 8.5.37 8.5.38 8.5.39 All 222 releases
wpvr / vendor / posthog / posthog-php / lib / Client.php

Client.php in WPVR – 360 Panorama viewer and Virtual Tour Builder for WordPress 8.5.37, at vendor/posthog/posthog-php/lib/Client.php

732 lines 21.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace PostHog;
4
5 use Exception;
6 use PostHog\Consumer\File;
7 use PostHog\Consumer\ForkCurl;
8 use PostHog\Consumer\LibCurl;
9 use PostHog\Consumer\Socket;
10
11 const SIZE_LIMIT = 50_000;
12
13 class Client
14 {
15 private const CONSUMERS = [
16 "socket" => Socket::class,
17 "file" => File::class,
18 "fork_curl" => ForkCurl::class,
19 "lib_curl" => LibCurl::class,
20 ];
21
22
23 /**
24 * @var string
25 */
26 private $apiKey;
27
28 /**
29 * @var string
30 */
31 private $personalAPIKey;
32
33 /**
34 * @var integer
35 */
36 private $featureFlagsRequestTimeout;
37
38 /**
39 * Consumer object handles queueing and bundling requests to PostHog.
40 *
41 * @var Consumer
42 */
43 protected $consumer;
44
45 /**
46 * @var HttpClient
47 */
48 public $httpClient;
49
50 /**
51 * @var array
52 */
53 public $featureFlags;
54
55 /**
56 * @var array
57 */
58 public $groupTypeMapping;
59
60 /**
61 * @var array
62 */
63 public $cohorts;
64
65
66 /**
67 * @var SizeLimitedHash
68 */
69 public $distinctIdsFeatureFlagsReported;
70
71 /**
72 * Create a new posthog object with your app's API key
73 * key
74 *
75 * @param string $apiKey
76 * @param array $options array of consumer options [optional]
77 * @param HttpClient|null $httpClient
78 */
79 public function __construct(
80 string $apiKey,
81 array $options = [],
82 ?HttpClient $httpClient = null,
83 ?string $personalAPIKey = null,
84 bool $loadFeatureFlags = true,
85 ) {
86 $this->apiKey = $apiKey;
87 $this->personalAPIKey = $personalAPIKey;
88 $Consumer = self::CONSUMERS[$options["consumer"] ?? "lib_curl"];
89 $this->consumer = new $Consumer($apiKey, $options, $httpClient);
90 $this->httpClient = $httpClient !== null ? $httpClient : new HttpClient(
91 $options['host'] ?? "app.posthog.com",
92 $options['ssl'] ?? true,
93 (int) ($options['maximum_backoff_duration'] ?? 10000),
94 false,
95 $options["debug"] ?? false,
96 null,
97 (int) ($options['timeout'] ?? 10000)
98 );
99 $this->featureFlagsRequestTimeout = (int) ($options['feature_flag_request_timeout_ms'] ?? 3000);
100 $this->featureFlags = [];
101 $this->groupTypeMapping = [];
102 $this->cohorts = [];
103 $this->distinctIdsFeatureFlagsReported = new SizeLimitedHash(SIZE_LIMIT);
104
105 // Populate featureflags and grouptypemapping if possible
106 if (
107 count($this->featureFlags) == 0
108 && !is_null($this->personalAPIKey)
109 && $loadFeatureFlags
110 ) {
111 $this->loadFlags();
112 }
113 }
114
115 public function __destruct()
116 {
117 $this->consumer->__destruct();
118 }
119
120 /**
121 * Captures a user action
122 *
123 * @param array $message
124 * @return bool whether the capture call succeeded
125 */
126 public function capture(array $message)
127 {
128 $message = $this->message($message);
129 $message["type"] = "capture";
130
131 if (array_key_exists('$groups', $message)) {
132 $message["properties"]['$groups'] = $message['$groups'];
133 }
134
135 $extraProperties = [];
136 $flags = [];
137 if (array_key_exists("send_feature_flags", $message) && $message["send_feature_flags"]) {
138 $flags = $this->fetchFeatureVariants($message["distinct_id"], $message["groups"]);
139 } elseif (count($this->featureFlags) != 0) {
140 # Local evaluation is enabled, flags are loaded, so try and get all flags we can without going to the server
141 $flags = $this->getAllFlags($message["distinct_id"], $message["groups"], [], [], true);
142 }
143
144 // Add all feature variants to event
145 foreach ($flags as $flagKey => $flagValue) {
146 $extraProperties[sprintf('$feature/%s', $flagKey)] = $flagValue;
147 }
148
149 // Add all feature flag keys that aren't false to $active_feature_flags
150 // decide v2 does this automatically, but we need it for when we upgrade to v3
151 $extraProperties['$active_feature_flags'] = array_keys(array_filter($flags, function ($flagValue) {
152 return $flagValue !== false;
153 }));
154
155 $message["properties"] = array_merge($extraProperties, $message["properties"]);
156
157 return $this->consumer->capture($message);
158 }
159
160 /**
161 * Tags properties about the user.
162 *
163 * @param array $message
164 * @return bool whether the identify call succeeded
165 */
166 public function identify(array $message)
167 {
168 if (isset($message['properties'])) {
169 $message['$set'] = $message['properties'];
170 }
171
172 $message = $this->message($message);
173 $message["type"] = "identify";
174 $message["event"] = '$identify';
175
176 return $this->consumer->identify($message);
177 }
178
179 /**
180 * decide if the feature flag is enabled for this distinct id.
181 *
182 * @param string $key
183 * @param string $distinctId
184 * @param array $groups
185 * @param array $personProperties
186 * @param array $groupProperties
187 * @return bool
188 * @throws Exception
189 */
190 public function isFeatureEnabled(
191 string $key,
192 string $distinctId,
193 array $groups = array(),
194 array $personProperties = array(),
195 array $groupProperties = array(),
196 bool $onlyEvaluateLocally = false,
197 bool $sendFeatureFlagEvents = true
198 ): null | bool {
199 $result = $this->getFeatureFlag(
200 $key,
201 $distinctId,
202 $groups,
203 $personProperties,
204 $groupProperties,
205 $onlyEvaluateLocally,
206 $sendFeatureFlagEvents
207 );
208
209 if (is_null($result)) {
210 return $result;
211 } else {
212 return boolval($result);
213 }
214 }
215
216 /**
217 * get the feature flag value for this distinct id.
218 *
219 * @param string $key
220 * @param string $distinctId
221 * @param array $groups
222 * @param array $personProperties
223 * @param array $groupProperties
224 * @return bool | string
225 * @throws Exception
226 */
227 public function getFeatureFlag(
228 string $key,
229 string $distinctId,
230 array $groups = array(),
231 array $personProperties = array(),
232 array $groupProperties = array(),
233 bool $onlyEvaluateLocally = false,
234 bool $sendFeatureFlagEvents = true
235 ): null | bool | string {
236 [$personProperties, $groupProperties] = $this->addLocalPersonAndGroupProperties(
237 $distinctId,
238 $groups,
239 $personProperties,
240 $groupProperties
241 );
242 $result = null;
243
244 foreach ($this->featureFlags as $flag) {
245 if ($flag["key"] == $key) {
246 try {
247 $result = $this->computeFlagLocally(
248 $flag,
249 $distinctId,
250 $groups,
251 $personProperties,
252 $groupProperties
253 );
254 } catch (InconclusiveMatchException $e) {
255 $result = null;
256 } catch (Exception $e) {
257 $result = null;
258 error_log("[PostHog][Client] Error while computing variant:" . $e->getMessage());
259 }
260 }
261 }
262
263 $flagWasEvaluatedLocally = !is_null($result);
264 $requestId = null;
265 $flagDetail = null;
266
267 if (!$flagWasEvaluatedLocally && !$onlyEvaluateLocally) {
268 try {
269 $response = $this->fetchFlagsResponse($distinctId, $groups, $personProperties, $groupProperties);
270 $requestId = isset($response['requestId']) ? $response['requestId'] : null;
271 $flagDetail = isset($response['flags'][$key]) ? $response['flags'][$key] : null;
272 $featureFlags = $response['featureFlags'] ?? [];
273 if (array_key_exists($key, $featureFlags)) {
274 $result = $featureFlags[$key];
275 } else {
276 $result = null;
277 }
278 } catch (Exception $e) {
279 error_log("[PostHog][Client] Unable to get feature variants:" . $e->getMessage());
280 $result = null;
281 }
282 }
283
284 if ($sendFeatureFlagEvents && !$this->distinctIdsFeatureFlagsReported->contains($key, $distinctId)) {
285 $properties = [
286 '$feature_flag' => $key,
287 '$feature_flag_response' => $result,
288 ];
289
290 if (!is_null($requestId)) {
291 $properties['$feature_flag_request_id'] = $requestId;
292 }
293
294 if (!is_null($flagDetail)) {
295 $properties['$feature_flag_id'] = $flagDetail['metadata']['id'];
296 $properties['$feature_flag_version'] = $flagDetail['metadata']['version'];
297 $properties['$feature_flag_reason'] = $flagDetail['reason']['description'];
298 }
299
300 $this->capture([
301 "properties" => $properties,
302 "distinct_id" => $distinctId,
303 "event" => '$feature_flag_called',
304 '$groups' => $groups
305 ]);
306 $this->distinctIdsFeatureFlagsReported->add($key, $distinctId);
307 }
308
309 if (!is_null($result)) {
310 return $result;
311 }
312 return null;
313 }
314
315 /**
316 * @param string $key
317 * @param string $distinctId
318 * @param array $groups
319 * @param array $personProperties
320 * @param array $groupProperties
321 * @return mixed
322 */
323 public function getFeatureFlagPayload(
324 string $key,
325 string $distinctId,
326 array $groups = array(),
327 array $personProperties = array(),
328 array $groupProperties = array(),
329 ): mixed {
330 $results = json_decode(
331 $this->flags($distinctId, $groups, $personProperties, $groupProperties),
332 true
333 );
334
335 if (isset($results['featureFlags'][$key]) === false || $results['featureFlags'][$key] !== true) {
336 return null;
337 }
338
339 $payload = $results['featureFlagPayloads'][$key] ?? null;
340
341 if ($payload === null) {
342 return null;
343 }
344
345 # feature flag payloads are always JSON encoded strings.
346 return json_decode($payload, true);
347 }
348
349 /**
350 * get the feature flag value for this distinct id.
351 *
352 * @param string $distinctId
353 * @param array $groups
354 * @param array $personProperties
355 * @param array $groupProperties
356 * @return array
357 * @throws Exception
358 */
359 public function getAllFlags(
360 string $distinctId,
361 array $groups = array(),
362 array $personProperties = array(),
363 array $groupProperties = array(),
364 bool $onlyEvaluateLocally = false
365 ): array {
366 [$personProperties, $groupProperties] = $this->addLocalPersonAndGroupProperties(
367 $distinctId,
368 $groups,
369 $personProperties,
370 $groupProperties
371 );
372 $response = [];
373 $fallbackToFlags = false;
374
375 if (count($this->featureFlags) > 0) {
376 foreach ($this->featureFlags as $flag) {
377 try {
378 $response[$flag['key']] = $this->computeFlagLocally(
379 $flag,
380 $distinctId,
381 $groups,
382 $personProperties,
383 $groupProperties
384 );
385 } catch (InconclusiveMatchException $e) {
386 $fallbackToFlags = true;
387 } catch (Exception $e) {
388 $fallbackToFlags = true;
389 error_log("[PostHog][Client] Error while computing variant:" . $e->getMessage());
390 }
391 }
392 } else {
393 $fallbackToFlags = true;
394 }
395
396 if ($fallbackToFlags && !$onlyEvaluateLocally) {
397 try {
398 $featureFlags = $this->fetchFeatureVariants($distinctId, $groups, $personProperties, $groupProperties);
399 $response = array_merge($response, $featureFlags);
400 } catch (Exception $e) {
401 error_log("[PostHog][Client] Unable to get feature variants:" . $e->getMessage());
402 }
403 }
404
405 return $response;
406 }
407
408 private function computeFlagLocally(
409 array $featureFlag,
410 string $distinctId,
411 array $groups = array(),
412 array $personProperties = array(),
413 array $groupProperties = array()
414 ): bool | string {
415 if ($featureFlag["ensure_experience_continuity"] ?? false) {
416 throw new InconclusiveMatchException("Flag has experience continuity enabled");
417 }
418
419 if (!$featureFlag["active"]) {
420 return false;
421 }
422
423 $flagFilters = $featureFlag["filters"] ?? [];
424 $aggregationGroupTypeIndex = $flagFilters["aggregation_group_type_index"] ?? null;
425
426 if (!is_null($aggregationGroupTypeIndex)) {
427 $groupName = $this->groupTypeMapping[strval($aggregationGroupTypeIndex)] ?? null;
428
429 if (is_null($groupName)) {
430 throw new InconclusiveMatchException("Flag has unknown group type index");
431 }
432
433 if (!array_key_exists($groupName, $groups)) {
434 return false;
435 }
436
437 $focusedGroupProperties = $groupProperties[$groupName];
438 return FeatureFlag::matchFeatureFlagProperties($featureFlag, $groups[$groupName], $focusedGroupProperties);
439 } else {
440 return FeatureFlag::matchFeatureFlagProperties($featureFlag, $distinctId, $personProperties, $this->cohorts);
441 }
442 }
443
444
445 /**
446 * @param string $distinctId
447 * @param array $groups
448 * @return array of feature flags
449 * @throws Exception
450 */
451 public function fetchFeatureVariants(
452 string $distinctId,
453 array $groups = [],
454 array $personProperties = [],
455 array $groupProperties = []
456 ): array {
457 $response = $this->fetchFlagsResponse($distinctId, $groups, $personProperties, $groupProperties);
458 return $response['featureFlags'] ?? [];
459 }
460
461 /**
462 * @param string $distinctId
463 * @param array $groups
464 * @return array of feature flags
465 * @throws Exception
466 */
467 private function fetchFlagsResponse(
468 string $distinctId,
469 array $groups = [],
470 array $personProperties = [],
471 array $groupProperties = []
472 ): ?array {
473 return json_decode(
474 $this->flags($distinctId, $groups, $personProperties, $groupProperties),
475 true
476 );
477 }
478
479 /**
480 * @throws Exception
481 */
482
483 public function loadFlags()
484 {
485 $payload = json_decode($this->localFlags(), true);
486
487 if ($payload && array_key_exists("detail", $payload)) {
488 throw new Exception($payload["detail"]);
489 }
490
491 $this->featureFlags = $payload['flags'] ?? [];
492 $this->groupTypeMapping = $payload['group_type_mapping'] ?? [];
493 $this->cohorts = $payload['cohorts'] ?? [];
494 }
495
496
497 public function localFlags()
498 {
499 return $this->httpClient->sendRequest(
500 '/api/feature_flag/local_evaluation?send_cohorts&token=' . $this->apiKey,
501 null,
502 [
503 // Send user agent in the form of {library_name}/{library_version} as per RFC 7231.
504 "User-Agent: posthog-php/" . PostHog::VERSION,
505 "Authorization: Bearer " . $this->personalAPIKey
506 ]
507 )->getResponse();
508 }
509
510 private function normalizeFeatureFlags(string $response): string
511 {
512 $decoded = json_decode($response, true);
513 if (isset($decoded['flags']) && !empty($decoded['flags'])) {
514 // This is a v4 response, we need to transform it to a v3 response for backwards compatibility
515 $transformedFlags = [];
516 $transformedPayloads = [];
517 foreach ($decoded['flags'] as $key => $flag) {
518 if ($flag['variant'] !== null) {
519 $transformedFlags[$key] = $flag['variant'];
520 } else {
521 $transformedFlags[$key] = $flag['enabled'] ?? false;
522 }
523 if (isset($flag['metadata']['payload'])) {
524 $transformedPayloads[$key] = $flag['metadata']['payload'];
525 }
526 }
527 $decoded['featureFlags'] = $transformedFlags;
528 $decoded['featureFlagPayloads'] = $transformedPayloads;
529 return json_encode($decoded);
530 }
531
532 return $response;
533 }
534
535 public function flags(
536 string $distinctId,
537 array $groups = array(),
538 array $personProperties = [],
539 array $groupProperties = []
540 ) {
541 $payload = array(
542 'api_key' => $this->apiKey,
543 'distinct_id' => $distinctId,
544 );
545
546 if (!empty($groups)) {
547 $payload["groups"] = $groups;
548 }
549
550 if (!empty($personProperties)) {
551 $payload["person_properties"] = $personProperties;
552 }
553
554 if (!empty($groupProperties)) {
555 $payload["group_properties"] = $groupProperties;
556 }
557
558 $response = $this->httpClient->sendRequest(
559 '/flags/?v=2',
560 json_encode($payload),
561 [
562 // Send user agent in the form of {library_name}/{library_version} as per RFC 7231.
563 "User-Agent: posthog-php/" . PostHog::VERSION,
564 ],
565 [
566 "shouldRetry" => false,
567 "timeout" => $this->featureFlagsRequestTimeout
568 ]
569 )->getResponse();
570
571 return $this->normalizeFeatureFlags($response);
572 }
573
574 /**
575 * Aliases from one user id to another
576 *
577 * @param array $message
578 * @return boolean whether the alias call succeeded
579 */
580 public function alias(array $message)
581 {
582 $message = $this->message($message);
583 $message["type"] = "alias";
584 $message["event"] = '$create_alias';
585
586 $message['properties']['distinct_id'] = $message['distinct_id'];
587 $message['properties']['alias'] = $message['alias'];
588
589 $message['distinct_id'] = null;
590 unset($message['alias']);
591
592 return $this->consumer->alias($message);
593 }
594
595 /**
596 * Queue a raw (prepared) message
597 *
598 * @param array $message
599 * @return mixed whether the identify call succeeded
600 */
601 public function raw(array $message)
602 {
603 return $this->consumer->enqueue($message);
604 }
605
606 /**
607 * Flush any async consumers
608 * @return boolean true if flushed successfully
609 */
610 public function flush()
611 {
612 if (method_exists($this->consumer, 'flush')) {
613 return $this->consumer->flush();
614 }
615
616 return true;
617 }
618
619 /**
620 * Formats a timestamp by making sure it is set
621 * and converting it to iso8601.
622 *
623 * The timestamp can be time in seconds `time()` or `microseconds(true)`.
624 * any other input is considered an error and the method will return a new date.
625 *
626 * Note: php's date() "u" format (for microseconds) has a bug in it
627 * it always shows `.000` for microseconds since `date()` only accepts
628 * ints, so we have to construct the date ourselves if microtime is passed.
629 *
630 * @param $ts
631 * @return false|string
632 */
633 private function formatTime($ts)
634 {
635 // time()
636 if (null == $ts || !$ts) {
637 $ts = time();
638 }
639 if (false !== filter_var($ts, FILTER_VALIDATE_INT)) {
640 return date("c", (int)$ts);
641 }
642
643 // anything else try to strtotime the date.
644 if (false === filter_var($ts, FILTER_VALIDATE_FLOAT)) {
645 if (is_string($ts)) {
646 return date("c", strtotime($ts));
647 }
648
649 return date("c");
650 }
651
652 // fix for floatval casting in send.php
653 $parts = explode(".", (string)$ts);
654 if (!isset($parts[1])) {
655 return date("c", (int)$parts[0]);
656 }
657
658 // microtime(true)
659 $sec = (int)$parts[0];
660 $usec = (int)$parts[1];
661 $fmt = sprintf("Y-m-d\\TH:i:s%sP", $usec);
662
663 return date($fmt, (int)$sec);
664 }
665
666 /**
667 * Add common fields to the given `message`
668 *
669 * @param array $msg
670 * @return array
671 */
672 private function message($msg)
673 {
674 if (!isset($msg["properties"])) {
675 $msg["properties"] = array();
676 }
677
678 $msg["library"] = 'posthog-php';
679 $msg["library_version"] = PostHog::VERSION;
680 $msg["library_consumer"] = $this->consumer->getConsumer();
681
682 $msg["properties"]['$lib'] = 'posthog-php';
683 $msg["properties"]['$lib_version'] = PostHog::VERSION;
684 $msg["properties"]['$lib_consumer'] = $this->consumer->getConsumer();
685
686 if (isset($msg["distinctId"])) {
687 $msg["distinct_id"] = $msg["distinctId"];
688 unset($msg["distinctId"]);
689 }
690
691 if (isset($msg["sendFeatureFlags"])) {
692 $msg["send_feature_flags"] = $msg["sendFeatureFlags"];
693 unset($msg["sendFeatureFlags"]);
694 }
695
696 if (!isset($msg["groups"])) {
697 $msg["groups"] = [];
698 }
699
700 if (!isset($msg["timestamp"])) {
701 $msg["timestamp"] = null;
702 }
703 $msg["timestamp"] = $this->formatTime($msg["timestamp"]);
704
705 return $msg;
706 }
707
708 private function addLocalPersonAndGroupProperties(
709 string $distinctId,
710 array $groups,
711 array $personProperties,
712 array $groupProperties
713 ): array {
714 $allPersonProperties = array_merge(
715 ["distinct_id" => $distinctId],
716 $personProperties
717 );
718
719 $allGroupProperties = [];
720 if (count($groups) > 0) {
721 foreach ($groups as $groupName => $groupValue) {
722 $allGroupProperties[$groupName] = array_merge(
723 ["\$group_key" => $groupValue],
724 $groupProperties[$groupName] ?? []
725 );
726 }
727 }
728
729 return [$allPersonProperties, $allGroupProperties];
730 }
731 }
732