├── .cursor
└── rules
│ └── cursor-rules-location.mdc
├── CHANGELOG.md
├── LICENSE.md
├── README.md
├── composer.json
├── config
└── segment.php
├── laravel-segment-banner.svg
├── phpstan.neon.dist
├── phpunit.xml.dist.bak
└── src
├── Contracts
├── CanBeIdentifiedForSegment.php
├── CanBeSentToSegment.php
├── CanNotifyViaSegment.php
├── SegmentServiceContract.php
└── ShouldBeAnonymouslyIdentified.php
├── Enums
└── SegmentPayloadType.php
├── Exceptions
├── NotifiableCannotBeIdentifiedForSegmentException.php
└── UnsupportedSegmentPayloadTypeException.php
├── Facades
├── Fakes
│ └── SegmentFake.php
└── Segment.php
├── LaravelSegmentServiceProvider.php
├── Middleware
└── ApplySegmentGlobals.php
├── Notifications
└── SegmentChannel.php
├── PendingUserSegment.php
├── SegmentService.php
├── SimpleSegmentAnonymousUser.php
├── SimpleSegmentEvent.php
├── SimpleSegmentIdentify.php
├── SimpleSegmentUser.php
├── Traits
└── HasSegmentIdentityByKey.php
└── ValueObjects
└── SegmentPayload.php
/.cursor/rules/cursor-rules-location.mdc:
--------------------------------------------------------------------------------
1 | ---
2 | description: Rules for placing and organizing Cursor rule files in the repository.
3 | globs: *.mdc
4 | ---
5 | # Cursor Rules Location
6 |
7 | Rules for placing and organizing Cursor rule files in the repository.
8 |
9 |
10 | name: cursor_rules_location
11 | description: Standards for placing Cursor rule files in the correct directory
12 | filters:
13 | # Match any .mdc files
14 | - type: file_extension
15 | pattern: "\\.mdc$"
16 | # Match files that look like Cursor rules
17 | - type: content
18 | pattern: "(?s).*?"
19 | # Match file creation events
20 | - type: event
21 | pattern: "file_create"
22 |
23 | actions:
24 | - type: reject
25 | conditions:
26 | - pattern: "^(?!\\.\\/\\.cursor\\/rules\\/.*\\.mdc$)"
27 | message: "Cursor rule files (.mdc) must be placed in the .cursor/rules directory"
28 |
29 | - type: suggest
30 | message: |
31 | When creating Cursor rules:
32 |
33 | 1. Always place rule files in PROJECT_ROOT/.cursor/rules/:
34 | ```
35 | .cursor/rules/
36 | ├── your-rule-name.mdc
37 | ├── another-rule.mdc
38 | └── ...
39 | ```
40 |
41 | 2. Follow the naming convention:
42 | - Use kebab-case for filenames
43 | - Always use .mdc extension
44 | - Make names descriptive of the rule's purpose
45 |
46 | 3. Directory structure:
47 | ```
48 | PROJECT_ROOT/
49 | ├── .cursor/
50 | │ └── rules/
51 | │ ├── your-rule-name.mdc
52 | │ └── ...
53 | └── ...
54 | ```
55 |
56 | 4. Never place rule files:
57 | - In the project root
58 | - In subdirectories outside .cursor/rules
59 | - In any other location
60 |
61 | examples:
62 | - input: |
63 | # Bad: Rule file in wrong location
64 | rules/my-rule.mdc
65 | my-rule.mdc
66 | .rules/my-rule.mdc
67 |
68 | # Good: Rule file in correct location
69 | .cursor/rules/my-rule.mdc
70 | output: "Correctly placed Cursor rule file"
71 |
72 | metadata:
73 | priority: high
74 | version: 1.0
75 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | All notable changes to `laravel-segment` will be documented in this file.
4 |
5 | ## 1.0.0 - 202X-XX-XX
6 |
7 | - initial release
8 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) SlashEquip
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Laravel Segment
2 |
3 | [](https://packagist.org/packages/slashequip/laravel-segment)
4 | [](https://github.com/slashequip/laravel-segment/actions/workflows/run-tests.yml)
5 | [](https://github.com/slashequip/laravel-segment/actions/workflows/php-cs-fixer.yml)
6 | [](https://github.com/slashequip/laravel-segment/actions/workflows/psalm.yml)
7 | [](https://packagist.org/packages/slashequip/laravel-segment)
8 |
9 | 
10 |
11 | Laravel Segment is an opinionated, approach to integrating Segment into your Laravel application.
12 |
13 |
14 | ## Installation
15 |
16 | You can install the package via composer:
17 |
18 | ```bash
19 | composer require slashequip/laravel-segment
20 | ```
21 |
22 |
23 | You can publish the config file with:
24 | ```bash
25 | php artisan vendor:publish --provider="SlashEquip\LaravelSegment\LaravelSegmentServiceProvider"
26 | ```
27 |
28 | This is the contents of the published config file, which should be located at `config/segment.php`:
29 |
30 | ```php
31 | return [
32 | 'enabled' => env('SEGMENT_ENABLED', true),
33 |
34 | /**
35 | * This is your Segment API write key. It can be
36 | * found under Source > Settings > Api Keys
37 | */
38 | 'write_key' => env('SEGMENT_WRITE_KEY', null),
39 |
40 | /**
41 | * Should the Segment service defer all tracking
42 | * api calls until after the response, sending
43 | * everything using the bulk/batch api?
44 | */
45 | 'defer' => env('SEGMENT_DEFER', false),
46 |
47 | /**
48 | * Should the Segment service be run in safe mode.
49 | * Safe mode will only report errors in sending
50 | * when safe mode is off exceptions are thrown
51 | */
52 | 'safe_mode' => env('SEGMENT_SAFE_MODE', true),
53 | ];
54 | ```
55 |
56 | ## Setting your write key
57 |
58 | Your write key is the API key given to you by Segment which can be found under your PHP source settings;
59 | `https://app.segment.com/{your-workspace-name}/sources/{your-source-name}/settings/keys` in the Segment UI.
60 |
61 | ## What is a Segment User
62 |
63 | When we talk about a 'user' in the context of this package we mean any object that
64 | implements the `SlashEquip\LaravelSegment\Contracts\CanBeIdentifiedForSegment` contract
65 | the package comes with a trait (and the interface) you can attach to your default
66 | User model;
67 |
68 | ```php
69 | use Illuminate\Database\Eloquent\Model;
70 | use SlashEquip\LaravelSegment\Traits\HasSegmentIdentityByKey;
71 | use SlashEquip\LaravelSegment\Contracts\CanBeIdentifiedForSegment;
72 |
73 | class User extends Model implements CanBeIdentifiedForSegment
74 | {
75 | use HasSegmentIdentityByKey;
76 | }
77 | ```
78 |
79 | Using this trait will automagically use your users' primary key as the identifier
80 | that is sent to Segment. Alternatively, you can implement your own instance of the
81 | `public function getSegmentIdentifier(): string;` method on your User model and not
82 | use the trait.
83 |
84 | ### Globally identifying users
85 |
86 | If you are sending Segment events in multiple places through your application and
87 | through-out a request it might make sense to globally identify a user to make it
88 | more convenient when making tracking calls.
89 |
90 | ```php
91 | use SlashEquip\LaravelSegment\Facades\Segment;
92 |
93 | Segment::setGlobalUser($user);
94 | ```
95 |
96 | ### Globally setting context
97 |
98 | Segment allows you to send [context](https://segment.com/docs/connections/spec/common/#context)
99 | with your tracking events too, you can set a global context that applies to all tracking events.
100 |
101 | ```php
102 | use SlashEquip\LaravelSegment\Facades\Segment;
103 |
104 | Segment::setGlobalContext([
105 | 'ip' => '127.0.0.1',
106 | 'locale' => 'en-US',
107 | 'screen' => [
108 | 'height' => 1080,
109 | 'width' => 1920,
110 | ],
111 | ]);
112 | ```
113 |
114 | ### Here have some convenience
115 |
116 | Laravel Segment ships with a middleware that you can apply in your HTTP Kernal that will handle
117 | the setting of the global user and some sensible global context too. It should be simple to extend
118 | this middleware and adjust for your needs if you want to add to the default context provided.
119 |
120 | ```php
121 | 'api' => [
122 | // ... other middleware
123 | SlashEquip\LaravelSegment\Middleware\ApplySegmentGlobals::class
124 | ],
125 | ```
126 |
127 | ## Usage
128 |
129 | ### For tracking events
130 | ```php
131 | use SlashEquip\LaravelSegment\Facades\Segment;
132 |
133 | Segment::forUser($user)->track('User Signed Up', [
134 | 'source' => 'Product Hunt',
135 | ]);
136 |
137 | // If you have set a global user you can
138 | // use the simpler provided syntax.
139 | Segment::track('User Signed Up', [
140 | 'source' => 'Product Hunt',
141 | ]);
142 |
143 | // If you have defer enabled in the config
144 | // you can still track an event immediately using trackNow.
145 | Segment::trackNow('User Signed Up', [
146 | 'source' => 'Product Hunt',
147 | ]);
148 | ```
149 |
150 | ### For identifying users
151 | ```php
152 | use SlashEquip\LaravelSegment\Facades\Segment;
153 |
154 | Segment::forUser($user)->identify([
155 | 'last_logged_in' => '2021-03-24 20:05:30',
156 | 'latest_subscription_amount' => '$24.60',
157 | ]);
158 |
159 | // If you have set a global user you can
160 | // use the simpler provided syntax.
161 | Segment::identify([
162 | 'last_logged_in' => '2021-03-24 20:05:30',
163 | 'latest_subscription_amount' => '$24.60',
164 | ]);
165 |
166 | // If you have defer enabled in the config
167 | // you can still identify a user immediately using identifyNow.
168 | Segment::identifyNow([
169 | 'source' => 'Product Hunt',
170 | ]);
171 | ```
172 |
173 | ### Anonymous users
174 |
175 | Segment allows you to track events for users that are not yet users in your system, they call these anonymous users.
176 |
177 | To track events for anonymous users you can use apply the `ShouldBeAnonymouslyIdentified` interface to any `CanBeIdentifiedForSegment` implementation.
178 |
179 | ```php
180 | use SlashEquip\LaravelSegment\Contracts\CanBeIdentifiedForSegment;
181 | use SlashEquip\LaravelSegment\Contracts\ShouldBeAnonymouslyIdentified;
182 |
183 | class SegmentAnonymousTestUser implements CanBeIdentifiedForSegment, ShouldBeAnonymouslyIdentified
184 | {
185 | }
186 | ```
187 |
188 | For convenience, the package comes with a `SimpleSegmentAnonymousUser` class that implements the `ShouldBeAnonymouslyIdentified` interface.
189 |
190 | ```php
191 | use SlashEquip\LaravelSegment\SimpleSegmentAnonymousUser;
192 |
193 | Segment::forUser(new SimpleSegmentAnonymousUser('123'))->track('Kitchen sink used');
194 | ```
195 |
196 | ### Laravel Notifications
197 | This package includes an out-of-the-box notification channel, to allow you to use Laravel's built-in notification
198 | feature. To send Segment events to users as notifications, generate your notification as normal;
199 |
200 | ```
201 | php artisan make:notification UserSubscribed
202 | ```
203 |
204 | You must ensure your notification implements the `CanNotifyViaSegment` interface, and add the required `toSegment`
205 | method. Then you can configure the `via` method to include the `SegmentChannel` class.
206 |
207 | You can then adjust the `toSegment` method to return the event you'd like.
208 |
209 | ```php
210 | use Illuminate\Notifications\Notification;
211 | use SlashEquip\LaravelSegment\Contracts\CanBeIdentifiedForSegment;
212 | use SlashEquip\LaravelSegment\Contracts\CanBeSentToSegment;
213 | use SlashEquip\LaravelSegment\Contracts\CanNotifyViaSegment;
214 | use SlashEquip\LaravelSegment\Notifications\SegmentChannel;
215 | use SlashEquip\LaravelSegment\SimpleSegmentEvent;
216 |
217 | class UserSubscribed extends Notification implements CanNotifyViaSegment
218 | {
219 | public function __construct(
220 | ) {
221 | }
222 |
223 | public function via(object $notifiable): array
224 | {
225 | return [SegmentChannel::class];
226 | }
227 |
228 | public function toSegment(CanBeIdentifiedForSegment $notifiable): CanBeSentToSegment
229 | {
230 | return new SimpleSegmentEvent(
231 | $notifiable,
232 | 'User Subscribed',
233 | [
234 | 'plan' => 'basic',
235 | 'team_name' => 'Funky chickens',
236 | ],
237 | );
238 | }
239 | }
240 | ```
241 |
242 | ## Misc
243 |
244 | ### Deferring
245 | When you start to fire many events in your application, even 2-3 per request it can be hyper-beneficial to
246 | turn on deferring (see config). When deferring is enabled, the service will store all of your tracking events triggered
247 | through-out the request or process and then send them in batch after your application has responded to your user. This
248 | happens during the Laravel termination.
249 |
250 | ### Safe mode
251 | By default, safe-mode is turned on. When safe-mode is active it will swallow any exceptions thrown when making the HTTP
252 | request to Segment and report them automatically to the exception handler, allow your app to continue running. When
253 | disabled then the exception will be thrown.
254 |
255 | ## Testing
256 |
257 | ```bash
258 | ./vendor/bin/pest
259 | ```
260 |
261 | ### Using `Segment::fake()` in tests
262 |
263 | To prevent real HTTP requests when running your own test suite, call
264 | `Segment::fake()` before executing the code under test. This swaps the
265 | Segment service with a fake that records events and identity calls. You can
266 | then use assertion helpers to verify what was sent:
267 |
268 | ```php
269 | use SlashEquip\LaravelSegment\Facades\Segment;
270 |
271 | Segment::fake();
272 |
273 | // Code that should trigger Segment events
274 |
275 | Segment::assertTracked('User Signed Up');
276 | Segment::assertIdentified();
277 | ```
278 |
279 | ## Changelog
280 |
281 | Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently.
282 |
283 | ## Contributing
284 |
285 | Please see [CONTRIBUTING](.github/CONTRIBUTING.md) for details.
286 |
287 | ## Security Vulnerabilities
288 |
289 | Please review [our security policy](../../security/policy) on how to report security vulnerabilities.
290 |
291 | ## Credits
292 |
293 | - [SlashEquip](https://github.com/slashequip)
294 | - [All Contributors](../../contributors)
295 |
296 | ## License
297 |
298 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information.
299 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "slashequip/laravel-segment",
3 | "description": "Laravel Segment is an opinionated, approach to integrating Segment into your Laravel application.",
4 | "keywords": [
5 | "laravel-segment"
6 | ],
7 | "homepage": "https://github.com/slashequip/laravel-segment",
8 | "license": "MIT",
9 | "authors": [
10 | {
11 | "name": "Sam Jones",
12 | "email": "sam@slashequip.com",
13 | "role": "Developer"
14 | }
15 | ],
16 | "require": {
17 | "php": "^8.2",
18 | "guzzlehttp/guzzle": "^7.8",
19 | "illuminate/contracts": "^11.0|^12.0"
20 | },
21 | "require-dev": {
22 | "larastan/larastan": "^3.3",
23 | "laravel/pint": "^1.18",
24 | "mockery/mockery": "^1.6",
25 | "orchestra/testbench": "^9.0|^10.0",
26 | "pestphp/pest": "^3.7"
27 | },
28 | "autoload": {
29 | "psr-4": {
30 | "SlashEquip\\LaravelSegment\\": "src"
31 | }
32 | },
33 | "autoload-dev": {
34 | "psr-4": {
35 | "SlashEquip\\LaravelSegment\\Tests\\": "tests"
36 | }
37 | },
38 | "scripts": {
39 | "analyse": "vendor/bin/phpstan",
40 | "format": "vendor/bin/pint",
41 | "test": "vendor/bin/pest"
42 | },
43 | "config": {
44 | "sort-packages": true,
45 | "allow-plugins": {
46 | "pestphp/pest-plugin": true
47 | }
48 | },
49 | "extra": {
50 | "laravel": {
51 | "providers": [
52 | "SlashEquip\\LaravelSegment\\LaravelSegmentServiceProvider"
53 | ],
54 | "aliases": {
55 | "Segment": "SlashEquip\\LaravelSegment\\Facades\\Segment"
56 | }
57 | }
58 | },
59 | "minimum-stability": "dev",
60 | "prefer-stable": true
61 | }
62 |
--------------------------------------------------------------------------------
/config/segment.php:
--------------------------------------------------------------------------------
1 | env('SEGMENT_ENABLED', true),
5 |
6 | /**
7 | * This is your Segment API write key. It can be
8 | * found under Source > Settings > Api Keys
9 | */
10 | 'write_key' => env('SEGMENT_WRITE_KEY', null),
11 |
12 | /**
13 | * Should the Segment service defer all tracking
14 | * api calls until after the response, sending
15 | * everything using the bulk/batch api?
16 | */
17 | 'defer' => env('SEGMENT_DEFER', false),
18 |
19 | /**
20 | * Should the Segment service be run in safe mode.
21 | * Safe mode will only report errors in sending
22 | * when safe mode is off exceptions are thrown
23 | */
24 | 'safe_mode' => env('SEGMENT_SAFE_MODE', true),
25 | ];
26 |
--------------------------------------------------------------------------------
/laravel-segment-banner.svg:
--------------------------------------------------------------------------------
1 |
12 |
--------------------------------------------------------------------------------
/phpstan.neon.dist:
--------------------------------------------------------------------------------
1 | includes:
2 | - ./vendor/larastan/larastan/extension.neon
3 |
4 | parameters:
5 |
6 | paths:
7 | - src/
8 |
9 | # Level 9 is the highest level
10 | level: 6
11 |
--------------------------------------------------------------------------------
/phpunit.xml.dist.bak:
--------------------------------------------------------------------------------
1 |
2 |
21 |
22 |
23 | tests
24 |
25 |
26 |
27 |
28 | ./src
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
--------------------------------------------------------------------------------
/src/Contracts/CanBeIdentifiedForSegment.php:
--------------------------------------------------------------------------------
1 | $globalContext
13 | */
14 | public function setGlobalContext(array $globalContext): void;
15 |
16 | /**
17 | * @param array $eventData
18 | */
19 | public function track(string $event, ?array $eventData = null): void;
20 |
21 | /**
22 | * @param array $eventData
23 | */
24 | public function trackNow(string $event, ?array $eventData = null): void;
25 |
26 | /**
27 | * @param array $identifyData
28 | */
29 | public function identify(?array $identifyData = null): void;
30 |
31 | /**
32 | * @param array $identifyData
33 | */
34 | public function identifyNow(?array $identifyData = null): void;
35 |
36 | public function forUser(CanBeIdentifiedForSegment $user): PendingUserSegment;
37 |
38 | public function push(CanBeSentToSegment $segment): void;
39 |
40 | public function terminate(): void;
41 | }
42 |
--------------------------------------------------------------------------------
/src/Contracts/ShouldBeAnonymouslyIdentified.php:
--------------------------------------------------------------------------------
1 | */
20 | private array $context = [];
21 |
22 | /** @var array */
23 | private array $events = [];
24 |
25 | /** @var array */
26 | private array $identities = [];
27 |
28 | public function setGlobalUser(CanBeIdentifiedForSegment $globalUser): void
29 | {
30 | $this->user = $globalUser;
31 | }
32 |
33 | /**
34 | * @param array $globalContext
35 | */
36 | public function setGlobalContext(array $globalContext): void
37 | {
38 | $this->context = $globalContext;
39 | }
40 |
41 | /**
42 | * @param array $identifyData
43 | */
44 | public function identify(?array $identifyData = null): void
45 | {
46 | $this->identities[] = new SimpleSegmentIdentify(
47 | $this->user,
48 | $identifyData
49 | );
50 | }
51 |
52 | /**
53 | * @param array $identifyData
54 | */
55 | public function identifyNow(?array $identifyData = null): void
56 | {
57 | $this->identities[] = new SimpleSegmentIdentify(
58 | $this->user,
59 | $identifyData
60 | );
61 | }
62 |
63 | /**
64 | * @param array $eventData
65 | */
66 | public function track(string $event, ?array $eventData = null): void
67 | {
68 | $this->events[] = new SimpleSegmentEvent(
69 | $this->user,
70 | $event,
71 | $eventData
72 | );
73 | }
74 |
75 | /**
76 | * @param array $eventData
77 | */
78 | public function trackNow(string $event, ?array $eventData = null): void
79 | {
80 | $this->events[] = new SimpleSegmentEvent(
81 | $this->user,
82 | $event,
83 | $eventData
84 | );
85 | }
86 |
87 | public function forUser(CanBeIdentifiedForSegment $user): PendingUserSegment
88 | {
89 | $this->user = $user;
90 |
91 | return new PendingUserSegment($this, $user);
92 | }
93 |
94 | public function push(CanBeSentToSegment $segment): void
95 | {
96 | if ($segment instanceof SimpleSegmentIdentify) {
97 | $this->identities[] = $segment;
98 | }
99 |
100 | if ($segment instanceof SimpleSegmentEvent) {
101 | $this->events[] = $segment;
102 | }
103 | }
104 |
105 | public function terminate(): void {}
106 |
107 | public function assertIdentified(Closure|int|null $callback = null): void
108 | {
109 | if (is_numeric($callback)) {
110 | $this->assertIdentifiedTimes($callback);
111 |
112 | return;
113 | }
114 |
115 | PHPUnit::assertTrue(
116 | $this->identities($callback)->count() > 0,
117 | 'The expected identities were not called.'
118 | );
119 | }
120 |
121 | public function assertIdentifiedTimes(int $times = 1): void
122 | {
123 | $count = collect($this->identities)->count();
124 |
125 | PHPUnit::assertSame(
126 | $times,
127 | $count,
128 | "The identity was called {$count} times instead of {$times} times."
129 | );
130 | }
131 |
132 | public function assertNotIdentified(?Closure $callback = null): void
133 | {
134 | PHPUnit::assertCount(
135 | 0,
136 | $this->identities($callback),
137 | 'The unexpected identity was called.'
138 | );
139 | }
140 |
141 | public function assertNothingIdentified(): void
142 | {
143 | $identities = collect($this->identities);
144 |
145 | PHPUnit::assertEmpty(
146 | $identities->all(),
147 | $identities->count().' events were found unexpectedly.'
148 | );
149 | }
150 |
151 | public function assertTracked(Closure|int|null $callback = null): void
152 | {
153 | if (is_numeric($callback)) {
154 | $this->assertTrackedTimes($callback);
155 |
156 | return;
157 | }
158 |
159 | PHPUnit::assertTrue(
160 | $this->events($callback)->count() > 0,
161 | 'The expected events were not called.'
162 | );
163 | }
164 |
165 | public function assertTrackedTimes(int $times = 1): void
166 | {
167 | $count = collect($this->events)->count();
168 |
169 | PHPUnit::assertSame(
170 | $times,
171 | $count,
172 | "The event called {$count} times instead of {$times} times."
173 | );
174 | }
175 |
176 | public function assertEventTracked(
177 | string $event,
178 | Closure|int|null $callback = null
179 | ): void {
180 | PHPUnit::assertTrue(
181 | $this->events($callback, $event)->count() > 0,
182 | 'The expected events were not called.'
183 | );
184 | }
185 |
186 | public function assertNotTracked(?Closure $callback = null): void
187 | {
188 | PHPUnit::assertCount(
189 | 0,
190 | $this->events($callback),
191 | 'The unexpected event was called.'
192 | );
193 | }
194 |
195 | public function assertEventNotTracked(
196 | string $event,
197 | Closure|int|null $callback = null
198 | ): void {
199 | PHPUnit::assertCount(
200 | 0,
201 | $this->events($callback, $event),
202 | 'The expected events were not called.'
203 | );
204 | }
205 |
206 | public function assertNothingTracked(): void
207 | {
208 | $events = collect($this->events);
209 |
210 | PHPUnit::assertEmpty(
211 | $events->all(),
212 | $events->count().' events were found unexpectedly.'
213 | );
214 | }
215 |
216 | /**
217 | * @return array|null
218 | */
219 | public function getContext(): ?array
220 | {
221 | return $this->context;
222 | }
223 |
224 | /**
225 | * @return Collection
226 | */
227 | private function identities(?Closure $callback = null): Collection
228 | {
229 | $identities = collect($this->identities);
230 |
231 | if ($identities->isEmpty()) {
232 | return collect();
233 | }
234 |
235 | $callback = $callback ?: fn () => true;
236 |
237 | return $identities->filter(
238 | fn (SimpleSegmentIdentify $identity) => $callback($identity)
239 | );
240 | }
241 |
242 | /**
243 | * @return Collection
244 | */
245 | private function events(
246 | ?Closure $callback = null,
247 | ?string $event = null
248 | ): Collection {
249 | $events = collect($this->events);
250 |
251 | if ($events->isEmpty()) {
252 | return collect();
253 | }
254 |
255 | $callback = $callback ?: fn () => true;
256 |
257 | return $events
258 | ->when($event, function (Collection $collection) use ($event) {
259 | return $collection->filter(function (
260 | SimpleSegmentEvent $segmentEvent
261 | ) use ($event) {
262 | return $segmentEvent->toSegment()->event === $event;
263 | });
264 | })
265 | ->filter(fn (SimpleSegmentEvent $event) => $callback($event));
266 | }
267 | }
268 |
--------------------------------------------------------------------------------
/src/Facades/Segment.php:
--------------------------------------------------------------------------------
1 | $globalContext)
16 | * @method static void track(string $event, ?array $eventData = null)
17 | * @method static void trackNow(string $event, ?array $eventData = null)
18 | * @method static void identify(?array $identifyData = null)
19 | * @method static void identifyNow(?array $identifyData = null)
20 | * @method static PendingUserSegment forUser(CanBeIdentifiedForSegment $user)
21 | * @method static void push(CanBeSentToSegment $segment)
22 | * @method static void terminate()
23 | * @method static void assertIdentified(Closure|int|null $callback = null)
24 | * @method static void assertIdentifiedTimes(int $times)
25 | * @method static void assertNotIdentified(Closure $callback = null)
26 | * @method static void assertNothingIdentified()
27 | * @method static void assertTracked(Closure|int|null $callback = null)
28 | * @method static void assertTrackedTimes(int $times)
29 | * @method static void assertEventTracked(string $event,Closure|int|null $callback = null)
30 | * @method static void assertNotTracked(Closure $callback = null)
31 | * @method static void assertNothingTracked()
32 | *
33 | * @see \SlashEquip\LaravelSegment\SegmentService
34 | * @see SegmentFake
35 | */
36 | class Segment extends Facade
37 | {
38 | protected static function getFacadeAccessor(): string
39 | {
40 | return SegmentServiceContract::class;
41 | }
42 |
43 | public static function fake(): SegmentFake
44 | {
45 | return tap(new SegmentFake, function (SegmentFake $fake) {
46 | static::swap($fake);
47 | });
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/LaravelSegmentServiceProvider.php:
--------------------------------------------------------------------------------
1 | publishes([
16 | __DIR__.'/../config/segment.php' => config_path('segment.php'),
17 | ]);
18 |
19 | /**
20 | * Send deferred tracking events to Segment after the response has been sent.
21 | *
22 | * @psalm-suppress UndefinedInterfaceMethod
23 | */
24 | $this->app->terminating(function () {
25 | Segment::terminate();
26 | });
27 |
28 | // Send deferred tracking events to Segment after a job has been processed.
29 | Queue::after(function () {
30 | Segment::terminate();
31 | });
32 | }
33 |
34 | public function register(): void
35 | {
36 | // Register config.
37 | $this->mergeConfigFrom(
38 | __DIR__.'/../config/segment.php',
39 | 'segment'
40 | );
41 |
42 | // Register the Segment service.
43 | $this->app->singleton(SegmentServiceContract::class, function ($app) {
44 | return new SegmentService($app->make('config')['segment']);
45 | });
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/Middleware/ApplySegmentGlobals.php:
--------------------------------------------------------------------------------
1 | user()) {
19 | /** @var CanBeIdentifiedForSegment $user */
20 | Segment::setGlobalUser($user);
21 | }
22 |
23 | /**
24 | * Build some nice default context based on the current request.
25 | */
26 | Segment::setGlobalContext($this->getContext($request));
27 |
28 | return $next($request);
29 | }
30 |
31 | /**
32 | * @return array
33 | */
34 | private function getContext(Request $request): array
35 | {
36 | return collect([
37 | 'ip' => $request->ip(),
38 | 'locale' => $request->getPreferredLanguage(),
39 | 'userAgent' => $request->userAgent(),
40 |
41 | /**
42 | * This is a solid default, generally backend calls
43 | * to Segment are not responsible to determining
44 | * whether or a not a user is active or not.
45 | */
46 | 'active' => false,
47 | ])
48 | ->filter(function ($context) {
49 | // Top level null values in the context
50 | // are meaningless at this point.
51 | return ! is_null($context);
52 | })
53 | ->all();
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/src/Notifications/SegmentChannel.php:
--------------------------------------------------------------------------------
1 | toSegment($notifiable));
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/PendingUserSegment.php:
--------------------------------------------------------------------------------
1 | |null $eventData
17 | */
18 | public function track(string $event, ?array $eventData = null): void
19 | {
20 | $this->service->push(
21 | new SimpleSegmentEvent($this->user, $event, $eventData)
22 | );
23 | }
24 |
25 | /**
26 | * @param array|null $eventData
27 | */
28 | public function trackNow(string $event, ?array $eventData = null): void
29 | {
30 | $this->service->push(
31 | new SimpleSegmentEvent($this->user, $event, $eventData)
32 | );
33 |
34 | $this->service->terminate();
35 | }
36 |
37 | /**
38 | * @param array|null $identifyData
39 | */
40 | public function identify(?array $identifyData = null): void
41 | {
42 | $this->service->push(
43 | new SimpleSegmentIdentify($this->user, $identifyData)
44 | );
45 | }
46 |
47 | /**
48 | * @param array|null $identifyData
49 | */
50 | public function identifyNow(?array $identifyData = null): void
51 | {
52 | $this->service->push(
53 | new SimpleSegmentIdentify($this->user, $identifyData)
54 | );
55 |
56 | $this->service->terminate();
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/src/SegmentService.php:
--------------------------------------------------------------------------------
1 | */
22 | private array $globalContext = [];
23 |
24 | /** @var array */
25 | private array $payloads = [];
26 |
27 | /**
28 | * @param array $config
29 | */
30 | public function __construct(
31 | private readonly array $config
32 | ) {}
33 |
34 | public function setGlobalUser(CanBeIdentifiedForSegment $globalUser): void
35 | {
36 | $this->globalUser = $globalUser;
37 | }
38 |
39 | /**
40 | * @param array $globalContext
41 | */
42 | public function setGlobalContext(array $globalContext): void
43 | {
44 | $this->globalContext = $globalContext;
45 | }
46 |
47 | /**
48 | * @param array $eventData
49 | */
50 | public function track(string $event, ?array $eventData = null): void
51 | {
52 | $this->push(
53 | new SimpleSegmentEvent($this->globalUser, $event, $eventData)
54 | );
55 | }
56 |
57 | /**
58 | * @param array $eventData
59 | */
60 | public function trackNow(string $event, ?array $eventData = null): void
61 | {
62 | $this->push(
63 | new SimpleSegmentEvent($this->globalUser, $event, $eventData)
64 | );
65 |
66 | $this->terminate();
67 | }
68 |
69 | /**
70 | * @param array $identifyData
71 | */
72 | public function identify(?array $identifyData = null): void
73 | {
74 | $this->push(
75 | new SimpleSegmentIdentify($this->globalUser, $identifyData)
76 | );
77 | }
78 |
79 | /**
80 | * @param array $identifyData
81 | */
82 | public function identifyNow(?array $identifyData = null): void
83 | {
84 | $this->push(
85 | new SimpleSegmentIdentify($this->globalUser, $identifyData)
86 | );
87 |
88 | $this->terminate();
89 | }
90 |
91 | public function forUser(CanBeIdentifiedForSegment $user): PendingUserSegment
92 | {
93 | return new PendingUserSegment($this, $user);
94 | }
95 |
96 | public function push(CanBeSentToSegment $segment): void
97 | {
98 | $this->payloads[] = $segment->toSegment();
99 |
100 | // We are not deferring so we send now!
101 | if (! $this->shouldDefer()) {
102 | $this->terminate();
103 | }
104 | }
105 |
106 | public function terminate(): void
107 | {
108 | if (empty($this->payloads)) {
109 | return;
110 | }
111 |
112 | if (! $this->isEnabled()) {
113 | $this->clean();
114 |
115 | return;
116 | }
117 |
118 | // Send the batch request.
119 | $response = Http::asJson()
120 | ->withToken(base64_encode($this->getWriteKey()))
121 | ->post(self::BATCH_URL, [
122 | 'batch' => $this->getBatchData(),
123 | 'context' => $this->globalContext,
124 | ]);
125 |
126 | // Do error handling.
127 | $this->handleResponseErrors($response);
128 |
129 | // Clean up.
130 | $this->clean();
131 | }
132 |
133 | /**
134 | * @return array
135 | */
136 | protected function getBatchData(): array
137 | {
138 | return collect($this->payloads)
139 | ->map(fn (SegmentPayload $payload) => $this->getDataFromPayload($payload))
140 | ->all();
141 | }
142 |
143 | /**
144 | * @return array
145 | */
146 | protected function getDataFromPayload(SegmentPayload $payload): array
147 | {
148 | $key = $payload->user instanceof ShouldBeAnonymouslyIdentified ? 'anonymousId' : 'userId';
149 |
150 | // Initial data.
151 | $data = [
152 | 'type' => $payload->type->value,
153 | $key => $payload->user->getSegmentIdentifier(),
154 | 'timestamp' => $payload->timestamp->format('Y-m-d\TH:i:s\Z'),
155 | ];
156 |
157 | // This is important, Segment will not handle empty
158 | // data arrays as expected and will drop the event.
159 | if (! empty($payload->data)) {
160 | $data[$payload->getDataKey()] = $payload->data;
161 | }
162 |
163 | // If it's a tracking call we need an event name!
164 | if ($payload->type === SegmentPayloadType::Track) {
165 | $data['event'] = $payload->event;
166 | }
167 |
168 | return $data;
169 | }
170 |
171 | protected function handleResponseErrors(Response $response): void
172 | {
173 | \rescue(
174 | callback: function () use ($response) {
175 | // If there was an error then it can be
176 | // thrown here and we can process.
177 | $response->throw();
178 | },
179 | rescue: function (Throwable $e) {
180 | // Cleanup and re-throw, we stop here.
181 | if (! $this->inSafeMode()) {
182 | $this->clean();
183 |
184 | throw $e;
185 | }
186 |
187 | // We report manually to prevent duplicate exceptions reports
188 | report($e);
189 | },
190 | report: false
191 | );
192 | }
193 |
194 | protected function clean(): void
195 | {
196 | $this->payloads = [];
197 | }
198 |
199 | protected function isEnabled(): bool
200 | {
201 | return filter_var($this->config['enabled'] ?? true, FILTER_VALIDATE_BOOL);
202 | }
203 |
204 | protected function getWriteKey(): string
205 | {
206 | return sprintf('%s:', $this->config['write_key'] ?? '');
207 | }
208 |
209 | protected function shouldDefer(): bool
210 | {
211 | return filter_var($this->config['defer'] ?? false, FILTER_VALIDATE_BOOL);
212 | }
213 |
214 | protected function inSafeMode(): bool
215 | {
216 | return filter_var($this->config['safe_mode'] ?? true, FILTER_VALIDATE_BOOL);
217 | }
218 | }
219 |
--------------------------------------------------------------------------------
/src/SimpleSegmentAnonymousUser.php:
--------------------------------------------------------------------------------
1 | $eventData
14 | */
15 | public function __construct(
16 | private readonly CanBeIdentifiedForSegment $user,
17 | private readonly string $event,
18 | private readonly ?array $eventData = null,
19 | ) {}
20 |
21 | public function toSegment(): SegmentPayload
22 | {
23 | return new SegmentPayload(
24 | user: $this->user,
25 | type: SegmentPayloadType::Track,
26 | event: $this->event,
27 | data: $this->eventData ?? [],
28 | );
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/SimpleSegmentIdentify.php:
--------------------------------------------------------------------------------
1 | $identifyData
14 | */
15 | public function __construct(
16 | private CanBeIdentifiedForSegment $user,
17 | private ?array $identifyData = null
18 | ) {}
19 |
20 | public function toSegment(): SegmentPayload
21 | {
22 | return new SegmentPayload(
23 | user: $this->user,
24 | type: SegmentPayloadType::Identify,
25 | data: $this->identifyData ?? [],
26 | );
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/SimpleSegmentUser.php:
--------------------------------------------------------------------------------
1 | id;
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/Traits/HasSegmentIdentityByKey.php:
--------------------------------------------------------------------------------
1 | getKey();
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/ValueObjects/SegmentPayload.php:
--------------------------------------------------------------------------------
1 | $data
16 | */
17 | public function __construct(
18 | public readonly CanBeIdentifiedForSegment $user,
19 | public readonly SegmentPayloadType $type,
20 | public readonly ?string $event = null,
21 | public readonly array $data = [],
22 | ?DateTime $timestamp = null
23 | ) {
24 | $this->timestamp = $timestamp ?: new DateTime;
25 | $this->timestamp->setTimezone(new DateTimeZone('UTC'));
26 | }
27 |
28 | public function getDataKey(): string
29 | {
30 | return match ($this->type) {
31 | SegmentPayloadType::Track => 'properties',
32 | SegmentPayloadType::Identify => 'traits',
33 | };
34 | }
35 | }
36 |
--------------------------------------------------------------------------------