├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── composer.json ├── config └── services.php ├── phpcs.xml ├── phpmd.xml ├── public ├── genealabs-laravel-mixpanel │ └── js │ │ └── mixpanel.js └── mix-manifest.json ├── resources ├── assets │ └── js │ │ └── mixpanel.js └── views │ └── partials │ └── mixpanel.blade.php ├── routes └── api.php └── src ├── Console └── Commands │ └── Publish.php ├── Events └── MixpanelEvent.php ├── Facades └── Mixpanel.php ├── Http ├── Controllers │ └── StripeWebhooksController.php └── Requests │ └── RecordStripeEvent.php ├── Interfaces └── DataCallback.php ├── LaravelMixpanel.php ├── Listeners ├── LaravelMixpanelUserObserver.php ├── Login.php ├── LoginAttempt.php ├── Logout.php └── MixpanelEvent.php └── Providers └── Service.php /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | All notable changes to this project will be documented in this file. 3 | This project adheres to [Semantic Versioning](http://semver.org/). 4 | 5 | ## TODO 6 | - http://phppackagechecklist.com/#1,2,3,4,5,6,9,10,11,12,13,14 7 | Complete check list items. 8 | - Detect through subscription create when a user had previously unsubscribed (churn), then resubscribes (unchurn). 9 | (This is already detected in subscription update.) 10 | - Filter any incoming web-hook events that are in test mode. 11 | 12 | ## [0.10.3] - 2020-08-05 13 | ### Added 14 | - configuration option for MixPanel Host. 15 | 16 | ## [0.10.1] - 2020-03-04 17 | ### Updated 18 | - Laravel dependency to release version. 19 | 20 | ## [0.10.0] - 2020-02-29 21 | ### Added 22 | - Laravel 7 compatibility. 23 | 24 | ## [0.9.1] - 2019-10-18 25 | ### Added 26 | - functionality to inject custom data points into track events. 27 | 28 | ### Fixed 29 | - Laravel 6.x compatibility. 30 | 31 | ## [0.9.0] - 2019-08-28 32 | ### Added 33 | - Laravel 6.0 compatibility. 34 | 35 | ## [0.8.1] - 2019-07-28 36 | ### Fixed 37 | - tracking of user information. 38 | 39 | ## [0.8.0] - 2019-04-05 40 | ### Added 41 | - Laravel 5.8 compatibility. 42 | 43 | ## [0.7.11] - 5 Oct 2018 44 | ### Added 45 | - Laravel 5.7 compatibility. 46 | 47 | ### Fixed 48 | - Stripe API compatibility. 49 | - disabling of default tracking. 50 | 51 | ## [0.7.4] - 7 Jan 2018 52 | ### Added 53 | - Laravel 5.6 compatibility. 54 | 55 | ## [0.7.4] - 7 Jan 2018 56 | ### Added 57 | - `Mixpanel::xxx()` facade. 58 | - `thanks` package as dev-dependency, as well as pretty phpunit printer package. 59 | 60 | ## [0.7.3] - 5 Nov 2017 61 | ### Added 62 | - initial integration tests. 63 | 64 | ### Changed 65 | - class structure as part of refactoring. 66 | 67 | ## [0.7.2] - 5 Nov 2017 68 | ### Fixed 69 | - inclusion of auto-track JS scripts. NPM library is broken and seems to not be maintained anymore, switched to script provided by mixpanel setup instructions. 70 | 71 | ## [0.7.1] - 18 Oct 2017 72 | ### Updated 73 | - user model listener to not fail if timestamps are turned off. 74 | 75 | ### Added 76 | - basic configuration in preparation for unit tests and code quality analysis. 77 | 78 | ## [0.7.0] - 31 Aug 2017 79 | ### Added 80 | - compatibility with Laravel 5.5. 81 | - auto-detection of package's service provider for automatic installation. 82 | - Console error to warn of missing ENV variable. 83 | 84 | ### Updated 85 | - README with L5.5 particulars. 86 | - CHANGELOG with new entries. 87 | 88 | ## [0.5.3] - 4 Apr 2016 89 | ### Added 90 | - configuration setting to disable the default-bundled tracking hooks. 91 | 92 | ## [0.5.2] - 16 Jan 2016 93 | ### Fixed 94 | - event listeners to properly detect new laravel 5.2 events. 95 | 96 | ## [0.5.1] - 11 Jan 2016 97 | ### Fixed 98 | - event listener to work with Laravel 5.2 core events. 99 | 100 | ## [0.4.10 - 0.4.12] - 28 Oct 2015 101 | ### Added 102 | - tracking of robots, if a browser isn't detected, and it is confirmed as a robot. 103 | 104 | ### Fixed 105 | - incorrect usage of array_filter, causing empty strings to be passed. 106 | 107 | ### Changed 108 | - reverted from using getAttribute(), as it is redundant. 109 | - refactored `track()` method to track: Url, Operating System, Hardware, Browser, Referring Domain, IP (For GeoLocation) 110 | 111 | ### Removed 112 | - any "unknown" values from being passed. 113 | 114 | ## [0.4.7 - 0.4.8] - 25 Oct 2015 115 | ### Changed 116 | - referenced to user model properties to use `getAttribute()` instead of referencing them directly. 117 | 118 | ### Added 119 | - tracking of `name` in addition to `first name` and `last name`. 120 | - tracking of `referrer` and `referring domain` to help get additional information for Live View (may not yet work 121 | fully). 122 | 123 | ### Fixed 124 | - improper tracking of user details, which caused users' names to be blank and a `0 Object [object]` field to be tracked 125 | erroneously. 126 | - fixed tracking to not depend on `$user->id` when referencing the primary key, but pulling it dynamically instead. 127 | 128 | ## [0.4.6] - 25 Sep 2015 129 | ### Fixed 130 | - documentation references to old Mixpanel class. 131 | - usage of first_name and last_name attributes on User classes that don't have them. 132 | 133 | ## [0.4.5] - 24 Sep 2015 134 | ### Fixed 135 | - Exception if no `$user->created_at` attribute exists. 136 | 137 | ## [0.4.0 - 0.4.4] - 13 Sep 2015 138 | ### Fixed 139 | - FQCN references in event handler. 140 | - namespace for controller in routes file. 141 | - namespace for service provider. 142 | - path to routes file. 143 | 144 | ### Changed 145 | - Move to new repository, change namespace. 146 | - Change referer detection. 147 | 148 | ## [0.3.12 - 0.3.13] - 2015-06-18 149 | ### Added 150 | - Identify user when tracking Page View if logged in. 151 | - Update "Last Seen" timestamp when logged-in user views a page. 152 | 153 | ## [0.3.1 - 0.3.11] - 2015-06-17 154 | ### Added 155 | - Referrer is now also recorded in Page View tracks. 156 | 157 | ### Removed 158 | - Temporarily disabled alias() until its purpose and usefulness is better assessed. 159 | 160 | ### Changed 161 | - Namespace HTTP/Controllers changed to Http/Controllers. 162 | - Fixed method to detect current URL. 163 | 164 | ### Fixed 165 | - Attempt at fixing client IP detection. 166 | - PHP version requirement updated to >5.5. 167 | - Fix namespace and path references. 168 | - Fix URL and Route detection for 'Page View' tracking. 169 | - Fixed detection of Page View elements to only be added if they exist. 170 | 171 | ## [0.3.0] - 2015-06-16 172 | ### Changed 173 | - Upgraded to Laravel 5.1 174 | 175 | ### Added 176 | - Page View tracking. 177 | - MixPanel alias() on registration to enabled proper funneling. 178 | 179 | ## [0.2.13] - 2015-06-10 180 | ### Added 181 | - Ignore transfer transactions. 182 | 183 | ## [0.2.11 - 0.2.12] - 2015-06-09 184 | ### Added 185 | - Additional check to detect stripe customer number. 186 | - Tightened sanity checks on subscription user detection. 187 | 188 | ## [0.2.10] - 2015-06-06 189 | ### Fixed 190 | - Extracted logic to get stripe customer id and throw exception if not found. 191 | 192 | ## [0.2.8 - 0.2.9] - 2015-06-05 193 | ### Fixed 194 | - Fixed logic error in customer ID parsing. 195 | 196 | ## [0.2.7] - 2015-06-04 197 | ### Fixed 198 | - Refactored subscription update functionality to be a little more robust. Testing all aspects of this has proven 199 | difficult, as Stripes webhook tests don't account for all variations possible in a given webhook request type. 200 | 201 | ## [0.2.6] - 2015-06-03 202 | ### Changed 203 | - Added FromPlan and ToPlan when tracking subscription changes. 204 | 205 | ### Fixed 206 | - Subscription updates check for previous subscription information, as not all subscription changes have it. 207 | 208 | ## [0.2.5] - 2015-06-2 209 | ### Fixed 210 | - Checked for existence of user when logging out before identifying with MixPanel. 211 | 212 | ## [0.1.6 - 0.2.4] - 2015-05-30 213 | ### Added 214 | - Webhook for tracking Stripe events. 215 | - Documented MixPanel events in README. 216 | 217 | ### Fixed 218 | - Sanitize People data before setting it, so that values don't get erased if something is not set. 219 | - Fix method parameters. 220 | - Track client IP address. 221 | - Ignore charge updates. 222 | - Formatting of non-existent dates during profile setting. 223 | - Detection of stripe customer id in webhook. 224 | 225 | ## [0.1.0 - 0.1.4] - 2015-05-30 226 | ### Changed 227 | - Updated `composer.json` details. 228 | - Updated README details. 229 | - Renamed events to model the formula , i.e. "Login Succeeded" or "Page Viewed". 230 | 231 | ### Fixed 232 | - Update profile information if not set (i.e. for prior existing users). 233 | - Log signup date as string, not as object. 234 | - Name detection on user model. 235 | 236 | ### Added 237 | - Initial package development. 238 | - User observer. 239 | - User events handler. 240 | - DocBlocks. 241 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at hello@genealabs.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to contribute 2 | We welcome everyone to submit pull requests with: 3 | - fixes for issues 4 | - change suggestions 5 | - updateing of documentation 6 | 7 | However, not every pull request will automatically be accepted. I will review each carefully to make sure it is in line with 8 | the direction I want the package to continue in. This might mean that some pull requests are not accepted, or might stay 9 | unmerged until a place for them can be determined. 10 | 11 | ## Testing 12 | - [ ] After making your changes, make sure the tests still pass. 13 | - [ ] When adding new functionality, also add new tests. 14 | - [ ] When fixing errors write and satisfy new unit tests that replicate the issue. 15 | - [ ] Make sure there are no build errors on our CI server (https://ci.genealabs.com/build-status/view/11) 16 | - [ ] All code must past PHPCS and PHPMD PSR2 validation. 17 | 18 | ## Submitting changes 19 | When submitting a pull request, it is important to make sure to complete the following: 20 | - [ ] Add a descriptive header that explains in a single sentence what problem the PR solves. 21 | - [ ] Add a detailed description with animated screen-grab GIFs visualizing how it works. 22 | - [ ] Explain why you think it should be implemented one way vs. another, highlight performance improvements, etc. 23 | 24 | ## Coding conventions 25 | Start reading our code and you'll get the hang of it. We optimize for readability: 26 | - indent using four spaces (soft tabs) 27 | - use Blade for all views 28 | - avoid logic in views, put it in controllers or service classes 29 | - ALWAYS put spaces after list items and method parameters (`[1, 2, 3]`, not `[1,2,3]`), around operators (`x += 1`, not `x+=1`), and around hash arrows. 30 | - this is open source software. Consider the people who will read your code, and make it look nice for them. It's sort of like driving a car: Perhaps you love doing donuts when you're alone, but with passengers the goal is to make the ride as smooth as possible. 31 | - emphasis readability of code over patterns to reduce mental debt 32 | - always add an empty line around structures (if statements, loops, etc.) 33 | 34 | Thanks! 35 | Mike Bronner, GeneaLabs 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 - 2017 GeneaLabs, Mike Bronner. 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 | # MixPanel for Laravel 2 | 3 | [![Scrutinizer](https://img.shields.io/scrutinizer/g/GeneaLabs/laravel-mixpanel.svg)](https://scrutinizer-ci.com/g/GeneaLabs/laravel-mixpanel) 4 | [![Coveralls](https://img.shields.io/coveralls/GeneaLabs/laravel-mixpanel.svg)](https://coveralls.io/github/GeneaLabs/laravel-mixpanel) 5 | [![GitHub (pre-)release](https://img.shields.io/github/release/GeneaLabs/laravel-mixpanel/all.svg)](https://github.com/GeneaLabs/laravel-mixpanel) 6 | [![Packagist](https://img.shields.io/packagist/dt/GeneaLabs/laravel-mixpanel.svg)](https://packagist.org/packages/genealabs/laravel-mixpanel) 7 | [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://raw.githubusercontent.com/GeneaLabs/laravel-mixpanel/master/LICENSE) 8 | 9 | ![Mixpanel for Laravel masthead image.](https://repository-images.githubusercontent.com/42419266/0f534200-f1b5-11e9-9ca7-57b0e1fe7764) 10 | 11 | ## Sponsors 12 | We like to thank the following sponsors for their generosity. Please take a moment to check them out. 13 | 14 | - [LIX](https://lix-it.com) 15 | 16 | ## Features 17 | - Asynchronous data transmission to Mixpanel's services. This prevents any 18 | delays to your application if Mixpanel is down, or slow to respond. 19 | - Drop-in installation and configuration into your Laravel app, tracking the 20 | most common events out of the box. 21 | - Simple Stripe integration allowing you to track revenues at the user level. 22 | - Front-end-ready Mixpanel JS library, both for Laravel Elixir inclusion or 23 | Blade template use. 24 | 25 | ## Requirements and Compatibility 26 | - PHP >= 7.2 27 | - Laravel >= 8.0 28 | 29 | ### Legacy Versions 30 | - [Laravel 5.2](https://github.com/GeneaLabs/laravel-mixpanel/tree/afcf3737412c1aebfa9dd1d7687001f78bdb3956) 31 | - [Laravel 5.0](https://github.com/GeneaLabs/laravel-mixpanel/tree/ce110ebd89658cbf8a91f2cfb5db57e2b449e7f3) 32 | 33 | ## Installation 34 | 1. Install the package: 35 | ```sh 36 | composer require genealabs/laravel-mixpanel 37 | ``` 38 | 2. Add your Mixpanel API Token to your `.env` file: 39 | ```env 40 | MIXPANEL_TOKEN=xxxxxxxxxxxxxxxxxxxxxx 41 | ``` 42 | 3. Add the MixPanel Host domain only if you need to change your MixPanel host from the default: 43 | ```env 44 | MIXPANEL_HOST=xxxxxxxxxxxxxxxxxxxxxx 45 | ``` 46 | 47 | ## Configuration 48 | ### Default Values 49 | - `services.mixpanel.host`: pulls the 'MIXPANEL_HOST' value from your `.env` 50 | file. 51 | - `services.mixpanel.token`: pulls the 'MIXPANEL_TOKEN' value from your `.env` 52 | file. 53 | - `services.mixpanel.enable-default-tracking`: (default: true) enable or disable 54 | Laravel user event tracking. 55 | - `services.mixpanel.consumer`: (default: socket) set the Guzzle adapter you 56 | want to use. 57 | - `services.mixpanel.connect-timeout`: (default: 2) set the number of seconds 58 | after which connections timeout. 59 | - `services.mixpanel.timeout`: (default: 2) set the number of seconds after 60 | which event tracking times out. 61 | - `services.mixpanel.data_callback_class`: (default: null) manipulate the data 62 | being passed back to mixpanel for the track events. 63 | 64 | ## Upgrade Notes 65 | ### Version 0.7.0 for Laravel 5.5 66 | - Remove the service provider from `/config/app.php`. The service provider is 67 | now auto-discovered in Laravel 5.5. 68 | 69 | ### Page Views 70 | - Page view tracking has been removed in favor of Mixpanels in-built Autotrack 71 | functionality, which tracks all page views. To turn it on, visit your 72 | Mixpanel dashboard, click *Applications > Autotrack > Web > etc.* and enable 73 | Autotracking. 74 | 75 | ## Usage 76 | MixPanel is loaded into the IoC as a singleton. This means you don't have to 77 | manually call $mixPanel::getInstance() as described in the MixPanel docs. 78 | This is already done for you in the ServiceProvider. 79 | 80 | Common user events are automatically recorded: 81 | - User Registration 82 | - User Deletion 83 | - User Login 84 | - User Login Failed 85 | - User Logoff 86 | - Cashier Subscribed 87 | - Cashier Payment Information Submitted 88 | - Cashier Subscription Plan Changed 89 | - Cashier Unsubscribed 90 | 91 | To make custom events, simple get MixPanel from the IoC using DI: 92 | ```php 93 | use GeneaLabs\LaravelMixpanel\LaravelMixpanel; 94 | 95 | class MyClass 96 | { 97 | protected $mixPanel; 98 | 99 | public function __construct(LaravelMixPanel $mixPanel) 100 | { 101 | $this->mixPanel = $mixPanel; 102 | } 103 | } 104 | ``` 105 | 106 | If DI is impractical in certain situations, you can also manually retrieve it 107 | from the IoC: 108 | ```php 109 | $mixPanel = app('mixpanel'); // using app helper 110 | $mixPanel = Mixpanel::getFacadeRoot(); // using facade 111 | ``` 112 | 113 | After that you can make the usual calls to the MixPanel API: 114 | - `$mixPanel->identify($user->id);` 115 | - `$mixPanel->track('User just paid!');` 116 | - `$mixPanel->people->trackCharge($user->id, '9.99');` 117 | - `$mixPanel->people->set($user->id, [$data]);` 118 | 119 | And so on ... 120 | 121 | ### Stripe Web-Hook 122 | If you wish to take advantage of the Stripe web-hook and track revenue per 123 | user, you should install Cashier: https://www.laravel.com/docs/5.5/billing 124 | 125 | Once that has been completed, exempt the web-hook endpoint from CSRF-validation 126 | in `/app/Http/Middleware/VerifyCsrfToken.php`: 127 | ```php 128 | protected $except = [ 129 | 'genealabs/laravel-mixpanel/stripe', 130 | ]; 131 | ``` 132 | 133 | The only other step remaining is to register the web-hook with Stripe: 134 | Log into your Stripe account: https://dashboard.stripe.com/dashboard, and open 135 | your account settings' webhook tab: 136 | 137 | Enter your MixPanel web-hook URL, similar to the following: `http:///genealabs/laravel-mixpanel/stripe`: 138 | ![screen shot 2015-05-31 at 1 35 01 pm](https://cloud.githubusercontent.com/assets/1791050/7903765/53ba6fe4-079b-11e5-9f92-a588bd81641d.png) 139 | 140 | Be sure to select "Live" if you are actually running live (otherwise put into test mode and update when you go live). 141 | Also, choose "Send me all events" to make sure Laravel Mixpanel can make full use of the Stripe data. 142 | 143 | ### JavaScript Events & Auto-Track 144 | #### Blade Template (Recommended) 145 | First publish the necessary assets: 146 | ```sh 147 | php artisan mixpanel:publish --assets 148 | ``` 149 | 150 | Then add the following to the head section of your layout template (already does 151 | the init call for you, using the token from your .env file): 152 | ```blade 153 | @include('genealabs-laravel-mixpanel::partials.mixpanel') 154 | ``` 155 | 156 | #### Laravel Elixir 157 | Add the following lines to your `/resources/js/app.js` (or equivalent), and 158 | don't forget to replace `YOUR_MIXPANEL_TOKEN` with your actual token: 159 | ```js 160 | require('./../../../public/genealabs-laravel-mixpanel/js/mixpanel.js'); 161 | mixpanel.init("YOUR_MIXPANEL_TOKEN"); 162 | ``` 163 | 164 | ### Laravel Integration 165 | Out of the box it will record the common events anyone would want to track. Also, if the default `$user->name` field is 166 | used that comes with Laravel, it will split up the name and use the last word as the last name, and everything prior for 167 | the first name. Otherwise it will look for `first_name` and `last_name` fields in the users table. 168 | 169 | - User registers: 170 | ``` 171 | Track: 172 | User: 173 | - Status: Registered 174 | People: 175 | - $first_name: 176 | - $last_name: 177 | - $email: 178 | - $created: 179 | ``` 180 | 181 | - User is updated: 182 | ``` 183 | People: 184 | - $first_name: 185 | - $last_name: 186 | - $email: 187 | - $created: 188 | ``` 189 | 190 | - User is deleted: 191 | ``` 192 | Track: 193 | User: 194 | - Status: Deactivated 195 | ``` 196 | 197 | - User is restored (from soft-deletes): 198 | ``` 199 | Track: 200 | User: 201 | - Status: Reactivated 202 | ``` 203 | 204 | - User logs in: 205 | ``` 206 | Track: 207 | Session: 208 | - Status: Logged In 209 | People: 210 | - $first_name: 211 | - $last_name: 212 | - $email: 213 | - $created: 214 | ``` 215 | 216 | - User login fails: 217 | ``` 218 | Track: 219 | Session: 220 | - Status: Login Failed 221 | People: 222 | - $first_name: 223 | - $last_name: 224 | - $email: 225 | - $created: 226 | ``` 227 | 228 | - User logs out: 229 | ``` 230 | Track: 231 | Session: 232 | - Status: Logged Out 233 | ``` 234 | ### Tracking Data Manipulation 235 | If you need to make changes or additions to the data being tracked, create a 236 | class that implements `\GeneaLabs\LaravelMixpanel\Interfaces\DataCallback`: 237 | 238 | ```php 239 | [ 260 | // ... 261 | "data_callback_class" => \App\MixpanelUserData::class, 262 | ] 263 | ``` 264 | 265 | ### Stripe Integration 266 | Many L5 sites are running Cashier to manage their subscriptions. This package creates an API webhook endpoint that keeps 267 | vital payment analytics recorded in MixPanel to help identify customer churn. 268 | 269 | Out of the box it will record the following Stripe events in MixPanel for you: 270 | 271 | #### Charges 272 | - Authorized Charge (when only authorizing a payment for a later charge date): 273 | ``` 274 | Track: 275 | Payment: 276 | - Status: Authorized 277 | - Amount: 278 | ``` 279 | 280 | - Captured Charge (when completing a previously authorized charge): 281 | ``` 282 | Track: 283 | Payment: 284 | - Status: Captured 285 | - Amount: 286 | People TrackCharge: 287 | ``` 288 | 289 | - Completed Charge: 290 | ``` 291 | Track: 292 | Payment: 293 | - Status: Successful 294 | - Amount: 295 | People TrackCharge: 296 | ``` 297 | 298 | - Refunded Charge: 299 | ``` 300 | Track: 301 | Payment: 302 | - Status: Refunded 303 | - Amount: 304 | People TrackCharge: - 305 | ``` 306 | 307 | - Failed Charge: 308 | ``` 309 | Track: 310 | Payment: 311 | - Status: Failed 312 | - Amount: 313 | ``` 314 | 315 | ### Subscriptions 316 | - Customer subscribed: 317 | ``` 318 | Track: 319 | Subscription: 320 | - Status: Created 321 | People: 322 | - Subscription: 323 | ``` 324 | 325 | - Customer unsubscribed: 326 | ``` 327 | Track: 328 | Subscription: 329 | - Status: Canceled 330 | - Upgraded: false 331 | Churn! :( 332 | People: 333 | - Subscription: None 334 | - Churned: 335 | - Plan When Churned: 336 | - Paid Lifetime: days 337 | ``` 338 | 339 | - Customer started trial: 340 | ``` 341 | Track: 342 | Subscription: 343 | - Status: Trial 344 | People: 345 | - Subscription: Trial 346 | ``` 347 | 348 | - Customer upgraded plan: 349 | ``` 350 | Track: 351 | Subscription: 352 | - Upgraded: true 353 | Unchurn! :-) 354 | People: 355 | - Subscription: 356 | ``` 357 | 358 | - Customer downgraded plan (based on dollar value compared to previous plan): 359 | ``` 360 | Track: 361 | Subscription: 362 | - Upgraded: false 363 | Churn! :-( 364 | People: 365 | - Subscription: 366 | - Churned: 367 | - Plan When Churned: 368 | ``` 369 | 370 | # The Fine Print 371 | ## Commitment to Quality 372 | During package development I try as best as possible to embrace good design and 373 | development practices to try to ensure that this package is as good as it can 374 | be. My checklist for package development includes: 375 | 376 | - ✅ Achieve as close to 100% code coverage as possible using unit tests. 377 | - ✅ Eliminate any issues identified by SensioLabs Insight and Scrutinizer. 378 | - ✅ Be fully PSR1, PSR2, and PSR4 compliant. 379 | - ✅ Include comprehensive documentation in README.md. 380 | - ✅ Provide an up-to-date CHANGELOG.md which adheres to the format outlined 381 | at . 382 | - ✅ Have no PHPMD or PHPCS warnings throughout all code. 383 | 384 | ## Contributing 385 | Please observe and respect all aspects of the included Code of Conduct . 386 | 387 | ### Reporting Issues 388 | When reporting issues, please fill out the included template as completely as 389 | possible. Incomplete issues may be ignored or closed if there is not enough 390 | information included to be actionable. 391 | 392 | ### Submitting Pull Requests 393 | Please review the Contribution Guidelines . 394 | Only PRs that meet all criterium will be accepted. 395 | 396 | ## ❤️ Open-Source Software - Give ⭐️ 397 | We have included the awesome `symfony/thanks` composer package as a dev 398 | dependency. Let your OS package maintainers know you appreciate them by starring 399 | the packages you use. Simply run composer thanks after installing this package. 400 | (And not to worry, since it's a dev-dependency it won't be installed in your 401 | live environment.) 402 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "genealabs/laravel-mixpanel", 3 | "description": "MixPanel wrapper for Laravel.", 4 | "license": "MIT", 5 | "authors": [ 6 | { 7 | "name": "Mike Bronner", 8 | "email": "hello@genealabs.com" 9 | } 10 | ], 11 | "autoload": { 12 | "psr-4": { 13 | "GeneaLabs\\LaravelMixpanel\\": "src/" 14 | } 15 | }, 16 | "require": { 17 | "illuminate/auth": "^10.0|^11.0|^12.0", 18 | "illuminate/config": "^10.0|^11.0|^12.0", 19 | "illuminate/console": "^10.0|^11.0|^12.0", 20 | "illuminate/events": "^10.0|^11.0|^12.0", 21 | "illuminate/http": "^10.0|^11.0|^12.0", 22 | "illuminate/queue": "^10.0|^11.0|^12.0", 23 | "illuminate/routing": "^10.0|^11.0|^12.0", 24 | "mixpanel/mixpanel-php": "^2.8", 25 | "sinergi/browser-detector": "^6.1" 26 | }, 27 | "require-dev": { 28 | "fakerphp/faker": "^1.8", 29 | "laravel/browser-kit-testing": "^7.0", 30 | "laravel/laravel": "^11.0", 31 | "laravel/ui": "^4.0", 32 | "php-coveralls/php-coveralls": "^2.4", 33 | "phpmd/phpmd": "^2.9", 34 | "phpunit/phpunit": "^10", 35 | "sebastian/phpcpd": "^7.0", 36 | "squizlabs/php_codesniffer": "^3.5", 37 | "symfony/thanks": "^1.2" 38 | }, 39 | "autoload-dev": { 40 | "psr-4": { 41 | "GeneaLabs\\LaravelMixpanel\\Tests\\": "tests/" 42 | } 43 | }, 44 | "extra": { 45 | "laravel": { 46 | "providers": [ 47 | "GeneaLabs\\LaravelMixpanel\\Providers\\Service" 48 | ], 49 | "alias": { 50 | "Mixpanel": "GeneaLabs\\LaravelMixpanel\\Facades\\Mixpanel" 51 | } 52 | } 53 | }, 54 | "minimum-stability": "dev", 55 | "prefer-stable": true, 56 | "config": { 57 | "allow-plugins": { 58 | "symfony/thanks": true 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /config/services.php: -------------------------------------------------------------------------------- 1 | [ 5 | 'host' => env("MIXPANEL_HOST"), 6 | 'token' => env('MIXPANEL_TOKEN'), 7 | 'enable-default-tracking' => true, 8 | 'consumer' => 'socket', 9 | 'connect-timeout' => 2, 10 | 'timeout' => 2, 11 | 'data_callback_class' => null, 12 | 'debug' => env('MIXPANEL_DEBUG', false), 13 | ] 14 | ]; 15 | -------------------------------------------------------------------------------- /phpcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | GeneaLabs coding standards. 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /phpmd.xml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikebronner/laravel-mixpanel/02e2339ce32c77aec307b0ebf1c767f4d09c3395/phpmd.xml -------------------------------------------------------------------------------- /public/genealabs-laravel-mixpanel/js/mixpanel.js: -------------------------------------------------------------------------------- 1 | !function(e,t){if(!t.__SV){var n,i,a=window;try{var p,o,r,l=a.location,s=l.hash;p=function(e,t){return(o=e.match(RegExp(t+"=([^&]*)")))?o[1]:null},s&&p(s,"state")&&"mpeditor"===(r=JSON.parse(decodeURIComponent(p(s,"state")))).action&&(a.sessionStorage.setItem("_mpcehash",s),history.replaceState(r.desiredHash||"",e.title,l.pathname+l.search))}catch(e){}window.mixpanel=t,t._i=[],t.init=function(e,a,p){function o(e,t){var n=t.split(".");2==n.length&&(e=e[n[0]],t=n[1]),e[t]=function(){e.push([t].concat(Array.prototype.slice.call(arguments,0)))}}var r=t;for(void 0!==p?r=t[p]=[]:p="mixpanel",r.people=r.people||[],r.toString=function(e){var t="mixpanel";return"mixpanel"!==p&&(t+="."+p),e||(t+=" (stub)"),t},r.people.toString=function(){return r.toString(1)+".people (stub)"},n="disable time_event track track_pageview track_links track_forms register register_once alias unregister identify name_tag set_config reset people.set people.set_once people.increment people.append people.union people.track_charge people.clear_charges people.delete_user".split(" "),i=0;i 2 | 9 | -------------------------------------------------------------------------------- /routes/api.php: -------------------------------------------------------------------------------- 1 | option('assets')) { 15 | $this->call('vendor:publish', [ 16 | '--provider' => Service::class, 17 | '--tag' => ['assets'], 18 | '--force' => true, 19 | ]); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Events/MixpanelEvent.php: -------------------------------------------------------------------------------- 1 | charge = $charge; 18 | $this->trackingData = $trackingData; 19 | $this->profileData = $profileData; 20 | $this->user = $user; 21 | } 22 | 23 | public function names() : Collection 24 | { 25 | return collect($this->trackingData) 26 | ->keys(); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Facades/Mixpanel.php: -------------------------------------------------------------------------------- 1 | process(); 12 | 13 | return response('', 204); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Http/Requests/RecordStripeEvent.php: -------------------------------------------------------------------------------- 1 | json()->all(); 24 | 25 | if (! $data || ! ($data['data'] ?? false)) { 26 | return; 27 | } 28 | 29 | $transaction = $data['data']['object']; 30 | $originalValues = array_key_exists('previous_attributes', $data['data']) 31 | ? $data['data']['previous_attributes'] 32 | : []; 33 | $stripeCustomerId = $this->findStripeCustomerId($transaction); 34 | $authModel = config('cashier.model') 35 | ?? config('auth.providers.users.model') 36 | ?? config('auth.model'); 37 | $user = app($authModel)->where('stripe_id', $stripeCustomerId)->first(); 38 | 39 | if (! $user) { 40 | return; 41 | } 42 | 43 | app('mixpanel')->identify($user->id); 44 | 45 | if ($transaction['object'] === 'charge' && ! count($originalValues)) { 46 | $this->recordCharge($transaction, $user); 47 | } 48 | 49 | if ($transaction['object'] === 'subscription') { 50 | $this->recordSubscription($transaction, $user, $originalValues); 51 | } 52 | } 53 | 54 | private function recordCharge(array $transaction, $user) 55 | { 56 | $charge = 0; 57 | $amount = $transaction['amount'] / 100; 58 | $status = 'Failed'; 59 | 60 | if ($transaction['paid']) { 61 | $status = 'Authorized'; 62 | 63 | if ($transaction['captured']) { 64 | $status = 'Successful'; 65 | 66 | if ($transaction['refunded']) { 67 | $status = 'Refunded'; 68 | } 69 | } 70 | } 71 | 72 | $trackingData = [ 73 | 'Payment' => [ 74 | 'Status' => $status, 75 | 'Amount' => $amount, 76 | ], 77 | ]; 78 | 79 | event(new MixpanelEvent($user, $trackingData, $charge)); 80 | } 81 | 82 | private function recordSubscription(array $transaction, $user, array $originalValues = []) 83 | { 84 | $profileData = []; 85 | $trackingData = []; 86 | $planStatus = array_key_exists('status', $transaction) ? $transaction['status'] : null; 87 | $planName = isset($transaction['plan']['name']) ? $transaction['plan']['name'] : null; 88 | $planStart = array_key_exists('start', $transaction) ? $transaction['start'] : null; 89 | $planAmount = isset($transaction['plan']['amount']) ? $transaction['plan']['amount'] : null; 90 | $oldPlanName = isset($originalValues['plan']['name']) ? $originalValues['plan']['name'] : null; 91 | $oldPlanAmount = isset($originalValues['plan']['amount']) ? $originalValues['plan']['amount'] : null; 92 | 93 | if ($planStatus === 'canceled') { 94 | $profileData = [ 95 | 'Subscription' => 'None', 96 | 'Churned' => (new Carbon) 97 | ->createFromTimestamp($transaction['canceled_at']) 98 | ->format('Y-m-d\Th:i:s'), 99 | 'Plan When Churned' => $planName, 100 | 'Paid Lifetime' => (new Carbon) 101 | ->createFromTimestampUTC($planStart) 102 | ->diffInDays((new Carbon)->createFromTimestamp($transaction['ended_at']) 103 | ->timezone('UTC')) . ' days' 104 | ]; 105 | $trackingData = [ 106 | 'Subscription' => ['Status' => 'Canceled', 'Upgraded' => false], 107 | 'Churn! :-(' => [], 108 | ]; 109 | } 110 | 111 | if (count($originalValues)) { 112 | if ($planAmount && $oldPlanAmount) { 113 | if ($planAmount < $oldPlanAmount) { 114 | $profileData = [ 115 | 'Subscription' => $planName, 116 | 'Churned' => (new Carbon($transaction['ended_at'])) 117 | ->timezone('UTC') 118 | ->format('Y-m-d\Th:i:s'), 119 | 'Plan When Churned' => $oldPlanName, 120 | ]; 121 | $trackingData = [ 122 | 'Subscription' => [ 123 | 'Upgraded' => false, 124 | 'FromPlan' => $oldPlanName, 125 | 'ToPlan' => $planName, 126 | ], 127 | 'Churn! :-(' => [], 128 | ]; 129 | } 130 | 131 | if ($planAmount > $oldPlanAmount) { 132 | $profileData = [ 133 | 'Subscription' => $planName, 134 | ]; 135 | $trackingData = [ 136 | 'Subscription' => [ 137 | 'Upgraded' => true, 138 | 'FromPlan' => $oldPlanName, 139 | 'ToPlan' => $planName, 140 | ], 141 | 'Unchurn! :-)' => [], 142 | ]; 143 | } 144 | } else { 145 | if ($planStatus === 'trialing' && ! $oldPlanName) { 146 | $profileData = [ 147 | 'Subscription' => $planName, 148 | ]; 149 | $trackingData = [ 150 | 'Subscription' => [ 151 | 'Upgraded' => true, 152 | 'FromPlan' => 'Trial', 153 | 'ToPlan' => $planName, 154 | ], 155 | 'Unchurn! :-)' => [], 156 | ]; 157 | } 158 | } 159 | } else { 160 | if ($planStatus === 'active') { 161 | $profileData = [ 162 | 'Subscription' => $planName, 163 | ]; 164 | $trackingData = [ 165 | 'Subscription' => ['Status' => 'Created'], 166 | ]; 167 | } 168 | 169 | if ($planStatus === 'trialing') { 170 | $profileData = [ 171 | 'Subscription' => 'Trial', 172 | ]; 173 | $trackingData = [ 174 | 'Subscription' => ['Status' => 'Trial'], 175 | ]; 176 | } 177 | } 178 | 179 | event(new MixpanelEvent($user, $trackingData, 0, $profileData)); 180 | } 181 | 182 | private function findStripeCustomerId(array $transaction) 183 | { 184 | if (array_key_exists('customer', $transaction)) { 185 | return $transaction['customer']; 186 | } 187 | 188 | if (array_key_exists('object', $transaction) && $transaction['object'] === 'customer') { 189 | return $transaction['id']; 190 | } 191 | 192 | if (array_key_exists('subscriptions', $transaction) 193 | && array_key_exists('data', $transaction['subscriptions']) 194 | && array_key_exists(0, $transaction['subscriptions']['data']) 195 | && array_key_exists('customer', $transaction['subscriptions']['data'][0]) 196 | ) { 197 | return $transaction['subscriptions']['data'][0]['customer']; 198 | } 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /src/Interfaces/DataCallback.php: -------------------------------------------------------------------------------- 1 | callbackResults = []; 21 | $this->defaults = [ 22 | 'consumer' => config('services.mixpanel.consumer', 'socket'), 23 | 'connect_timeout' => config('services.mixpanel.connect-timeout', 2), 24 | 'timeout' => config('services.mixpanel.timeout', 2), 25 | 'debug' => config('services.mixpanel.debug', false), 26 | ]; 27 | 28 | if (config('services.mixpanel.host')) { 29 | $this->defaults["host"] = config('services.mixpanel.host'); 30 | } 31 | 32 | $this->request = $request; 33 | 34 | 35 | parent::__construct( 36 | config('services.mixpanel.token'), 37 | array_merge($this->defaults, $options) 38 | ); 39 | } 40 | 41 | protected function getData() : array 42 | { 43 | $browserInfo = new Browser(); 44 | $osInfo = new Os(); 45 | $deviceInfo = new Device(); 46 | $browserVersion = trim(str_replace('unknown', '', $browserInfo->getName() . ' ' . $browserInfo->getVersion())); 47 | $osVersion = trim(str_replace('unknown', '', $osInfo->getName() . ' ' . $osInfo->getVersion())); 48 | $hardwareVersion = trim(str_replace('unknown', '', $deviceInfo->getName())); 49 | 50 | $data = [ 51 | 'Url' => $this->request->getUri(), 52 | 'Operating System' => $osVersion, 53 | 'Hardware' => $hardwareVersion, 54 | '$browser' => $browserVersion, 55 | 'Referrer' => $this->request->header('referer'), 56 | '$referring_domain' => ($this->request->header('referer') 57 | ? parse_url($this->request->header('referer'))['host'] 58 | : null), 59 | 'ip' => $this->request->ip(), 60 | ]; 61 | 62 | if ((! array_key_exists('$browser', $data)) && $browserInfo->isRobot()) { 63 | $data['$browser'] = 'Robot'; 64 | } 65 | 66 | return array_filter($data); 67 | } 68 | 69 | public function track($event, $properties = []) 70 | { 71 | $properties = array_filter($properties); 72 | $data = $properties + $this->getData(); 73 | 74 | if ($callbackClass = config("services.mixpanel.data_callback_class")) { 75 | $data = (new $callbackClass)->process($data); 76 | $data = array_filter($data); 77 | } 78 | 79 | parent::track($event, $data); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/Listeners/LaravelMixpanelUserObserver.php: -------------------------------------------------------------------------------- 1 | []])); 11 | } 12 | } 13 | 14 | public function saving($user) 15 | { 16 | if (config("services.mixpanel.enable-default-tracking")) { 17 | event(new Mixpanel($user, ['User: Updated' => []])); 18 | } 19 | } 20 | 21 | public function deleting($user) 22 | { 23 | if (config("services.mixpanel.enable-default-tracking")) { 24 | event(new Mixpanel($user, ['User: Deactivated' => []])); 25 | } 26 | } 27 | 28 | public function restored($user) 29 | { 30 | if (config("services.mixpanel.enable-default-tracking")) { 31 | event(new Mixpanel($user, ['User: Reactivated' => []])); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Listeners/Login.php: -------------------------------------------------------------------------------- 1 | user, ['User Logged In' => []])); 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/Listeners/LoginAttempt.php: -------------------------------------------------------------------------------- 1 | where('email', $email) 20 | ->first(); 21 | } 22 | 23 | event(new Mixpanel($user, ['Login Attempted' => []])); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Listeners/Logout.php: -------------------------------------------------------------------------------- 1 | user, ['User Logged Out' => []])); 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/Listeners/MixpanelEvent.php: -------------------------------------------------------------------------------- 1 | user; 11 | 12 | if ($user && config("services.mixpanel.enable-default-tracking")) { 13 | $profileData = $this->getProfileData($user); 14 | $profileData = array_merge($profileData, $event->profileData); 15 | 16 | app('mixpanel')->identify($user->getKey()); 17 | app('mixpanel')->people->set($user->getKey(), $profileData, request()->ip()); 18 | 19 | if ($event->charge !== 0) { 20 | app('mixpanel')->people->trackCharge($user->id, $event->charge); 21 | } 22 | 23 | foreach ($event->trackingData as $eventName => $data) { 24 | app('mixpanel')->track($eventName, $data); 25 | } 26 | } 27 | } 28 | 29 | private function getProfileData($user) : array 30 | { 31 | $firstName = $user->first_name; 32 | $lastName = $user->last_name; 33 | 34 | if ($user->name) { 35 | $nameParts = explode(' ', $user->name); 36 | array_filter($nameParts); 37 | $lastName = array_pop($nameParts); 38 | $firstName = implode(' ', $nameParts); 39 | } 40 | 41 | $data = [ 42 | '$first_name' => $firstName, 43 | '$last_name' => $lastName, 44 | '$name' => $user->name, 45 | '$email' => $user->email, 46 | '$created' => ($user->created_at 47 | ? (new Carbon()) 48 | ->parse($user->created_at) 49 | ->format('Y-m-d\Th:i:s') 50 | : null), 51 | ]; 52 | array_filter($data); 53 | 54 | return $data; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Providers/Service.php: -------------------------------------------------------------------------------- 1 | [MixpanelEventListener::class], 24 | Attempting::class => [LoginAttempt::class], 25 | Login::class => [LoginListener::class], 26 | Logout::class => [LogoutListener::class], 27 | ]; 28 | 29 | public function boot() 30 | { 31 | parent::boot(); 32 | 33 | include __DIR__ . '/../../routes/api.php'; 34 | 35 | $this->loadViewsFrom(__DIR__ . '/../../resources/views', 'genealabs-laravel-mixpanel'); 36 | $this->publishes([ 37 | __DIR__ . '/../../public' => public_path(), 38 | ], 'assets'); 39 | 40 | if (config('services.mixpanel.enable-default-tracking')) { 41 | $authModel = config('auth.providers.users.model') ?? config('auth.model'); 42 | $this->app->make($authModel)->observe(new LaravelMixpanelUserObserver()); 43 | } 44 | } 45 | 46 | public function register() 47 | { 48 | parent::register(); 49 | 50 | $this->mergeConfigFrom(__DIR__ . '/../../config/services.php', 'services'); 51 | $this->commands(Publish::class); 52 | $this->app->singleton('mixpanel', LaravelMixpanel::class); 53 | } 54 | 55 | public function provides() : array 56 | { 57 | return ['genealabs-laravel-mixpanel']; 58 | } 59 | } 60 | --------------------------------------------------------------------------------