From 7545695a641fe7203fe25535edc0b7d1c74d4b4b Mon Sep 17 00:00:00 2001 From: Tien Date: Tue, 23 Sep 2025 16:09:43 +0700 Subject: [PATCH 1/3] add organization into metadata for mixpanel --- src/Publishers/MixpanelPublisher.php | 113 +++++++++++--- tests/Unit/MixpanelPublisherTest.php | 219 +++++++++++++++++++++++++++ 2 files changed, 315 insertions(+), 17 deletions(-) create mode 100644 tests/Unit/MixpanelPublisherTest.php diff --git a/src/Publishers/MixpanelPublisher.php b/src/Publishers/MixpanelPublisher.php index d0a6040..79f52aa 100644 --- a/src/Publishers/MixpanelPublisher.php +++ b/src/Publishers/MixpanelPublisher.php @@ -4,10 +4,12 @@ use BetterWorld\Scribe\Contracts\Book; use BetterWorld\Scribe\Contracts\Metadata; +use BetterWorld\Scribe\Contracts\Narrative; use BetterWorld\Scribe\Contracts\Publisher; use Exception; use Mixpanel; +use Producers_MixpanelGroups; use function BetterWorld\Scribe\Support\array_value; final readonly class MixpanelPublisher implements Publisher @@ -35,28 +37,105 @@ public function name(): string public function publish(Book $book): bool { foreach ($book->read() as $narrative) { - $metadata = $narrative instanceof Metadata ? $narrative->metadata() : []; - try { - if (isset($metadata['user_id']) && is_string($metadata['user_id'])) { - $userId = $metadata['user_id']; - $this->mixpanel->identify(user_id: $userId); - - if (isset($metadata['properties']) && is_array($metadata['properties'])) { - $this->mixpanel->people->setOnce($userId, $metadata['properties']); - } - } - - $this->mixpanel->track( - event: (string) $narrative::key(), - properties: $narrative->values() - ); - + $this->publishNarrative($narrative); } catch (Exception $e) { - error_log('Mixpanel tracking failed: '.$e->getMessage()); + error_log('Mixpanel tracking failed: ' . $e->getMessage()); } } return true; } + + /** + * @param Narrative $narrative + */ + private function publishNarrative($narrative): void + { + $metadata = $narrative instanceof Metadata ? $narrative->metadata() : []; + $trackProperties = $narrative->values(); + + $userId = $this->handleUserMetadata($metadata); + $orgId = $this->handleOrganizationMetadata($metadata); + + $this->addContextToTrackProperties($trackProperties, $userId, $orgId); + $this->trackEvent($narrative, $trackProperties); + } + + /** + * @param array $metadata + */ + private function handleUserMetadata(array $metadata): ?string + { + if (!isset($metadata['user']) || !is_array($metadata['user'])) { + return null; + } + + $userData = $metadata['user']; + $userId = $userData['id'] ?? null; + + if (!is_string($userId)) { + return null; + } + + $this->mixpanel->identify(user_id: $userId); + + if (isset($userData['properties']) && is_array($userData['properties'])) { + $this->mixpanel->people->setOnce($userId, $userData['properties']); + } + + return $userId; + } + + /** + * @param array $metadata + */ + private function handleOrganizationMetadata(array $metadata): ?string + { + if (!isset($metadata['organization']) || !is_array($metadata['organization'])) { + return null; + } + + $orgData = $metadata['organization']; + $orgId = $orgData['id'] ?? null; + + if (!is_string($orgId)) { + return null; + } + + if (isset($orgData['properties']) && is_array($orgData['properties'])) { + /** @var Producers_MixpanelGroups $group */ + $group = $this->mixpanel->group; + $group->setOnce('organization_id', $orgId, $orgData['properties']); + } + + return $orgId; + } + + /** + * @param array $trackProperties + */ + private function addContextToTrackProperties(array &$trackProperties, ?string $userId, ?string $orgId): void + { + if ($userId) { + $trackProperties['distinct_id'] = $userId; + } + + if ($orgId) { + $trackProperties['organization_id'] = $orgId; + $trackProperties['$groups'] = ['organization_id' => $orgId]; + } + } + + /** + * @param Narrative $narrative + * @param array $trackProperties + */ + private function trackEvent(Narrative $narrative, array $trackProperties): void + { + $this->mixpanel->track( + event: $narrative::key(), + properties: $trackProperties + ); + } } diff --git a/tests/Unit/MixpanelPublisherTest.php b/tests/Unit/MixpanelPublisherTest.php new file mode 100644 index 0000000..dc7a502 --- /dev/null +++ b/tests/Unit/MixpanelPublisherTest.php @@ -0,0 +1,219 @@ + [], + 'default_book' => 'main', + 'books' => ['main' => ['publishers' => []]], + 'auto_publish' => false, + ]); +} + +function createMixpanelPublisher(string $token = 'test-token'): MixpanelPublisher +{ + return new MixpanelPublisher('test-mixpanel', createNarrativeService(), ['token' => $token]); +} + +/** + * @param array $values + * @param array|null $metadata + * @return \BetterWorld\Scribe\Contracts\Narrative&\BetterWorld\Scribe\Contracts\Metadata + */ +function createTestNarrative(string $key, array $values, ?array $metadata = null) +{ + return new class($values, $metadata) extends Narrative implements Metadata + { + /** + * @param array $eventValues + * @param array|null $eventMetadata + */ + public function __construct( + private array $eventValues, + private ?array $eventMetadata + ) {} + + public static function key(): string + { + return 'test_event'; + } + + /** + * @return array + */ + public function values(): array + { + return $this->eventValues; + } + + /** + * @return array + */ + public function metadata(): array + { + return $this->eventMetadata ?? []; + } + }; +} + +/** + * @param array<\BetterWorld\Scribe\Contracts\Narrative> $narratives + */ +function createTestBook(array $narratives): Book +{ + return new class($narratives) implements Book + { + /** + * @param array<\BetterWorld\Scribe\Contracts\Narrative> $narratives + */ + public function __construct(private array $narratives) {} + + public function name(): string + { + return 'test-book'; + } + + /** + * @return array<\BetterWorld\Scribe\Contracts\Narrative> + */ + public function read(): array + { + return $this->narratives; + } + + public function write(\BetterWorld\Scribe\Contracts\Narrative $narrative): static + { + $this->narratives[] = $narrative; + + return $this; + } + + public function publishers(): array + { + return []; + } + + public function publish(): void {} + }; +} + +test('mixpanel publisher has a name', function (): void { + $publisher = createMixpanelPublisher(); + + expect($publisher->name())->toBe('test-mixpanel'); +}); + +test('mixpanel publisher creates mixpanel instance with token', function (): void { + $publisher = createMixpanelPublisher(); + + expect($publisher)->toBeInstanceOf(MixpanelPublisher::class); +}); + +test('mixpanel publisher publishes narratives without metadata', function (): void { + $publisher = createMixpanelPublisher(); + $narrative = createTestNarrative('test_event', ['property1' => 'value1', 'property2' => 'value2']); + $book = createTestBook([$narrative]); + + $result = $publisher->publish($book); + + expect($result)->toBeTrue(); +}); + +test('mixpanel publisher publishes narratives with user metadata', function (): void { + $publisher = createMixpanelPublisher(); + $metadata = [ + 'user' => [ + 'id' => 'USER5', + 'properties' => [ + '$name' => 'Tien Le', + 'email' => 'tien@example.com', + ], + ], + ]; + $narrative = createTestNarrative('user_action', ['action' => 'login'], $metadata); + $book = createTestBook([$narrative]); + + $result = $publisher->publish($book); + + expect($result)->toBeTrue(); +}); + +test('mixpanel publisher publishes narratives with organization metadata', function (): void { + $publisher = createMixpanelPublisher(); + $metadata = [ + 'organization' => [ + 'id' => 'COMP7', + 'properties' => [ + 'Name' => 'BetterWorld', + 'Plan' => 'Enterprise', + ], + ], + ]; + $narrative = createTestNarrative('org_event', ['action' => 'org_created'], $metadata); + $book = createTestBook([$narrative]); + + $result = $publisher->publish($book); + + expect($result)->toBeTrue(); +}); + +test('mixpanel publisher publishes narratives with both user and organization metadata', function (): void { + $publisher = createMixpanelPublisher(); + $metadata = [ + 'user' => [ + 'id' => 'USER5', + 'properties' => [ + '$name' => 'Tien Le', + 'organization_id' => 'COMP7', + ], + ], + 'organization' => [ + 'id' => 'COMP7', + 'properties' => [ + 'Name' => 'BetterWorld', + ], + ], + ]; + $narrative = createTestNarrative('company_event', ['action' => 'login'], $metadata); + $book = createTestBook([$narrative]); + + $result = $publisher->publish($book); + + expect($result)->toBeTrue(); +}); + +test('mixpanel publisher handles multiple narratives in book', function (): void { + $publisher = createMixpanelPublisher(); + $narrative1 = createTestNarrative('event1', ['data' => 'value1']); + $narrative2 = createTestNarrative('event2', ['data' => 'value2']); + $book = createTestBook([$narrative1, $narrative2]); + + $result = $publisher->publish($book); + + expect($result)->toBeTrue(); +}); + +test('mixpanel publisher handles empty book', function (): void { + $publisher = createMixpanelPublisher(); + $book = createTestBook([]); + + $result = $publisher->publish($book); + + expect($result)->toBeTrue(); +}); + +test('mixpanel publisher handles exceptions gracefully', function (): void { + $publisher = createMixpanelPublisher('invalid-token'); + $narrative = createTestNarrative('test_event', ['data' => 'value']); + $book = createTestBook([$narrative]); + + $result = $publisher->publish($book); + + expect($result)->toBeTrue(); +}); From 684530cdd18848f4d5b04db5a6ed510beb820003 Mon Sep 17 00:00:00 2001 From: Tien Date: Tue, 23 Sep 2025 17:37:33 +0700 Subject: [PATCH 2/3] lint fix --- src/Publishers/MixpanelPublisher.php | 2 ++ tests/Unit/MixpanelPublisherTest.php | 31 +++++++++------------------- 2 files changed, 12 insertions(+), 21 deletions(-) diff --git a/src/Publishers/MixpanelPublisher.php b/src/Publishers/MixpanelPublisher.php index 79f52aa..22e6b43 100644 --- a/src/Publishers/MixpanelPublisher.php +++ b/src/Publishers/MixpanelPublisher.php @@ -6,6 +6,7 @@ use BetterWorld\Scribe\Contracts\Metadata; use BetterWorld\Scribe\Contracts\Narrative; use BetterWorld\Scribe\Contracts\Publisher; +use BetterWorld\Scribe\Exceptions\MissingArrayKeyException; use Exception; use Mixpanel; @@ -18,6 +19,7 @@ /** * @param array $options + * @throws MissingArrayKeyException */ public function __construct( private string $name, diff --git a/tests/Unit/MixpanelPublisherTest.php b/tests/Unit/MixpanelPublisherTest.php index dc7a502..45f889e 100644 --- a/tests/Unit/MixpanelPublisherTest.php +++ b/tests/Unit/MixpanelPublisherTest.php @@ -2,23 +2,12 @@ use BetterWorld\Scribe\Contracts\Book; use BetterWorld\Scribe\Contracts\Metadata; -use BetterWorld\Scribe\Narratives\Narrative; -use BetterWorld\Scribe\NarrativeService; +use BetterWorld\Scribe\Narrative; use BetterWorld\Scribe\Publishers\MixpanelPublisher; -function createNarrativeService(): NarrativeService -{ - return new NarrativeService([ - 'publishers' => [], - 'default_book' => 'main', - 'books' => ['main' => ['publishers' => []]], - 'auto_publish' => false, - ]); -} - function createMixpanelPublisher(string $token = 'test-token'): MixpanelPublisher { - return new MixpanelPublisher('test-mixpanel', createNarrativeService(), ['token' => $token]); + return new MixpanelPublisher('test-mixpanel', ['token' => $token]); } /** @@ -26,7 +15,7 @@ function createMixpanelPublisher(string $token = 'test-token'): MixpanelPublishe * @param array|null $metadata * @return \BetterWorld\Scribe\Contracts\Narrative&\BetterWorld\Scribe\Contracts\Metadata */ -function createTestNarrative(string $key, array $values, ?array $metadata = null) +function createTestNarrative(array $values, ?array $metadata = null) { return new class($values, $metadata) extends Narrative implements Metadata { @@ -117,7 +106,7 @@ public function publish(): void {} test('mixpanel publisher publishes narratives without metadata', function (): void { $publisher = createMixpanelPublisher(); - $narrative = createTestNarrative('test_event', ['property1' => 'value1', 'property2' => 'value2']); + $narrative = createTestNarrative(['property1' => 'value1', 'property2' => 'value2']); $book = createTestBook([$narrative]); $result = $publisher->publish($book); @@ -136,7 +125,7 @@ public function publish(): void {} ], ], ]; - $narrative = createTestNarrative('user_action', ['action' => 'login'], $metadata); + $narrative = createTestNarrative(['action' => 'login'], $metadata); $book = createTestBook([$narrative]); $result = $publisher->publish($book); @@ -155,7 +144,7 @@ public function publish(): void {} ], ], ]; - $narrative = createTestNarrative('org_event', ['action' => 'org_created'], $metadata); + $narrative = createTestNarrative(['action' => 'org_created'], $metadata); $book = createTestBook([$narrative]); $result = $publisher->publish($book); @@ -180,7 +169,7 @@ public function publish(): void {} ], ], ]; - $narrative = createTestNarrative('company_event', ['action' => 'login'], $metadata); + $narrative = createTestNarrative(['action' => 'login'], $metadata); $book = createTestBook([$narrative]); $result = $publisher->publish($book); @@ -190,8 +179,8 @@ public function publish(): void {} test('mixpanel publisher handles multiple narratives in book', function (): void { $publisher = createMixpanelPublisher(); - $narrative1 = createTestNarrative('event1', ['data' => 'value1']); - $narrative2 = createTestNarrative('event2', ['data' => 'value2']); + $narrative1 = createTestNarrative(['data' => 'value1']); + $narrative2 = createTestNarrative(['data' => 'value2']); $book = createTestBook([$narrative1, $narrative2]); $result = $publisher->publish($book); @@ -210,7 +199,7 @@ public function publish(): void {} test('mixpanel publisher handles exceptions gracefully', function (): void { $publisher = createMixpanelPublisher('invalid-token'); - $narrative = createTestNarrative('test_event', ['data' => 'value']); + $narrative = createTestNarrative(['data' => 'value']); $book = createTestBook([$narrative]); $result = $publisher->publish($book); From 54782da01c17b2fe14b6a9fc7156041eaa68e741 Mon Sep 17 00:00:00 2001 From: tienlethanh <39031310+tienlethanh@users.noreply.github.com> Date: Tue, 23 Sep 2025 10:38:19 +0000 Subject: [PATCH 3/3] fix quality errors --- src/Publishers/MixpanelPublisher.php | 24 ++++++++++++------------ tests/Unit/MixpanelPublisherTest.php | 8 ++++---- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/Publishers/MixpanelPublisher.php b/src/Publishers/MixpanelPublisher.php index 22e6b43..91de9af 100644 --- a/src/Publishers/MixpanelPublisher.php +++ b/src/Publishers/MixpanelPublisher.php @@ -9,8 +9,8 @@ use BetterWorld\Scribe\Exceptions\MissingArrayKeyException; use Exception; use Mixpanel; - use Producers_MixpanelGroups; + use function BetterWorld\Scribe\Support\array_value; final readonly class MixpanelPublisher implements Publisher @@ -19,6 +19,7 @@ /** * @param array $options + * * @throws MissingArrayKeyException */ public function __construct( @@ -42,7 +43,7 @@ public function publish(Book $book): bool try { $this->publishNarrative($narrative); } catch (Exception $e) { - error_log('Mixpanel tracking failed: ' . $e->getMessage()); + error_log('Mixpanel tracking failed: '.$e->getMessage()); } } @@ -50,7 +51,7 @@ public function publish(Book $book): bool } /** - * @param Narrative $narrative + * @param Narrative $narrative */ private function publishNarrative($narrative): void { @@ -65,18 +66,18 @@ private function publishNarrative($narrative): void } /** - * @param array $metadata + * @param array $metadata */ private function handleUserMetadata(array $metadata): ?string { - if (!isset($metadata['user']) || !is_array($metadata['user'])) { + if (! isset($metadata['user']) || ! is_array($metadata['user'])) { return null; } $userData = $metadata['user']; $userId = $userData['id'] ?? null; - if (!is_string($userId)) { + if (! is_string($userId)) { return null; } @@ -90,18 +91,18 @@ private function handleUserMetadata(array $metadata): ?string } /** - * @param array $metadata + * @param array $metadata */ private function handleOrganizationMetadata(array $metadata): ?string { - if (!isset($metadata['organization']) || !is_array($metadata['organization'])) { + if (! isset($metadata['organization']) || ! is_array($metadata['organization'])) { return null; } $orgData = $metadata['organization']; $orgId = $orgData['id'] ?? null; - if (!is_string($orgId)) { + if (! is_string($orgId)) { return null; } @@ -115,7 +116,7 @@ private function handleOrganizationMetadata(array $metadata): ?string } /** - * @param array $trackProperties + * @param array $trackProperties */ private function addContextToTrackProperties(array &$trackProperties, ?string $userId, ?string $orgId): void { @@ -130,8 +131,7 @@ private function addContextToTrackProperties(array &$trackProperties, ?string $u } /** - * @param Narrative $narrative - * @param array $trackProperties + * @param array $trackProperties */ private function trackEvent(Narrative $narrative, array $trackProperties): void { diff --git a/tests/Unit/MixpanelPublisherTest.php b/tests/Unit/MixpanelPublisherTest.php index 45f889e..49bb58a 100644 --- a/tests/Unit/MixpanelPublisherTest.php +++ b/tests/Unit/MixpanelPublisherTest.php @@ -13,9 +13,9 @@ function createMixpanelPublisher(string $token = 'test-token'): MixpanelPublishe /** * @param array $values * @param array|null $metadata - * @return \BetterWorld\Scribe\Contracts\Narrative&\BetterWorld\Scribe\Contracts\Metadata + * @return \BetterWorld\Scribe\Contracts\Narrative&Metadata */ -function createTestNarrative(array $values, ?array $metadata = null) +function createTestNarrative(array $values, ?array $metadata = null): object { return new class($values, $metadata) extends Narrative implements Metadata { @@ -24,8 +24,8 @@ function createTestNarrative(array $values, ?array $metadata = null) * @param array|null $eventMetadata */ public function __construct( - private array $eventValues, - private ?array $eventMetadata + private readonly array $eventValues, + private readonly ?array $eventMetadata ) {} public static function key(): string