Tracking Envoyer Releases in Sentry

Note: this article I wrote originally appeared on the Humaan blog.

Dan looking out from a Sentry tower

Tracking down bugs can be hard. Damn hard. And figuring out how or when they cropped up in the first place can be even harder. Recently, we launched our social media aggregator platform, Waaffle. It’s written in Laravel and deployed with Envoyer, but we also took the opportunity to try out Sentry for tracking bugs.

Spoiler alert: it was awesome.

Releases in a Sentry issue

One of the features I really like is the ability to track releases, which means you can tell at a glance which release was responsible for your bug. We’ve got a deployment hook in Envoyer that pings Sentry when we deploy a new release. Our hook runs in the “After” section of the “Activate New Release” action and contains the following command:

curl https://sentry.io/api/hooks/release/builtin/project_id/project_webhook_token/ \ -X POST -H 'Content-Type: application/json' -d '{"version": "{{ sha }}"}'

(FYI, it’s the very last post-deployment hook we run, just in case another hook fails.)

The Sentry PHP package includes a section in the config for specifying your app version. However, things can get tricky if, like us, you’re using, say, git commit hashes instead of version numbers.

Thankfully, Sentry’s Laravel package provides code for a nice release hash you can use for getting your git commit hash:

'release' => trim(exec('git log --pretty="%h" -n1 HEAD'))

Which if you run in Artisan’s tinker mode, gives you something like this:

1a2b3c

Now the problem: we use Envoyer for our deployments, which downloads the tarball of a specified branch/release/tag instead the entire git repository. That tarball doesn’t contain any of the git history in it, so it’s not technically a repository, thus running the Sentry Laravel command won’t give you a commit hash. If you try, you’ll get the following lovely error:

fatal: Not a git repository (or any of the parent directories): .git

Which means if Sentry tracks an error and saves it, you’ll see there’s no release attached to the issue. Drats!

Somehow, we need a way to get the current commit hash from our Envoyer deployments so we can tell Sentry how to get the current commit. Thankfully, Envoyer comes to the rescue!

In the Envoyer docs, under Deployment Hooks, you’ll see a section on how you can get the current git commit hash (or sha1 hash). Now, this hash is actually the first 12 characters of the current git commit hash – aka. something you can use in the deployment lifecycle.

From here, we want to target the current release directory, so let’s create a deployment hook in the “After” section of “Activate New Release” and call it “Write Git Hash to File” with the user “forge”:

echo "{{ sha }}" > {{release}}/.commit_hash
Our Envoyer hook

Pretty simple, huh? All it does is echoes the current git hash to stdout (standard output), then we redirect that output to a file called .commit_hash in the current release directory. The single arrow (>) means “set the file .commit_hash to 0 length, then append to it”. If we used two arrows (>>) we’d only append to the file, which we wouldn’t want to do – though technically it doesn’t really matter given the release directory has just been created, but it’s the principle of the thing!

Once we’ve got that file written on every deployment, we need to tell Sentry how to get that commit hash! If you wanted to, you could change the config so Sentry fetches the release, like so:

'release' => trim(exec('echo "$(< .commit_hash)"'))

(Note that I’m using echo and redirection here, I don’t want to get into the UUOC debate!)

Except this only works if you have that .commit_hash file in every environment, which we don’t. So, we’re stuck between a rock and a hard place. What we need is a solution that uses the .commit_hash file if it exists, or falls back to a git repository, or finally, returns null if neither of those exist.

In our project, we’ve got a small helpers file with all functions that exist in the global namespace. That’s where we put our solution:

Pretty basic really – just an if/elseif/else block with three possible outcomes. Note that I’m using the base_path Laravel helper, so we can be sure we’ve got the full path to the file/folder in question. Then, in our Sentry configuration file we have the following for the release:

'release' => get_commit_hash(),

Now whenever Sentry needs the commit hash, it’ll call that function and get its return value. Nice! And for those of you concerned about the overheads of getting the commit hash, you can do a php artisan config:cache so the hash is only calculated once. I highly recommend caching the app configuration as part of your deployment process to help to make things that little bit faster. That file is located in bootstrap/cache/config.php and you can verify the existence of this cached value by doing the following (in the root of the project, with the configuration cached):

grep -n --context=3 \'release\' bootstrap/cache/config.php

You should see something like this:

484-   'sentry' =>
485-   array (
486-     'dsn' => 'https://xxxxx:xxxxx@sentry.io/project_id',
487:     'release' => '1a2b3c4d5e6f',
488-   ),
489-   'auth' =>
490-   array (

(The line numbers to the left indicate where in the file it was found.)

Now you’re set! The next time you do a deployment, you’ll have Sentry correctly logging issues with the current release throughout the life of the project.

Happy tracking, folks!


Lessons I’ve learnt from developing a large Laravel project

For those of you who don’t know me, I’m a huge Laravel fan. I recently finished up a large project for a client, and I learnt many important things along the way. Below, you’ll see features of Laravel I used and loved in Laravel to make the development cycle much more enjoyable (and painless). By no means should you feel like you have to, these are merely just tips I’ve found that helped me.

Middleware is awesome – use it. Middlware allowed me to very easily filter out users who had been added to the portal, but had not activated their account. It also gave me the ability to block access to users who once had an account in this portal, but were now “archived” and no longer able to log in and make changes. Middleware is easily applied/removed to routes as you require, so use them when you can!

Form Request validators can help you save time. The Form Request classes allow you to easily abstract your form submissions into a separate step/file which in my opinion, makes for cleaner code. The portal I developed had a number of different models, and having one validation Form Request for each made it so easily to ensure the data submitted would be validated correctly. Type hinting is amazing.

Home(stead) is where the heart is. Just in case you didn’t know, Homestead is a pre-configured development environment for Vagrant that makes it painless to develop your PHP site with a modern toolset. Technically, Homestead is created for Laravel, but you can use it for any PHP project! I have the VMware Integration for Vagrant which I find to be a fair bit faster, but if you’re just checking Homestead out , the VirtualBox adapter should be more than enough.

Eloquent accessors and mutators are fantastic. Instead of jamming your create/update methods with formatting of different attributes of a model, use mutators and accessors to automatically handle all those little tasks. For example, in a project I needed an attribute to be stored in the database as JSON, but when accessed within the project, it’s automatically formatted to an array by the Eloquent model. When that array is modified throughout the lifetime of the application and then saved back to the database, it’s converted to a JSON string. It can be tempting to include validation in mutators, but I would recommend against doing that. For the most part, use it as a way to convert or format one thing to another.

Forge and Envoyer are a match made in heaven. When it came to deploying this project, I had the opportunity to deploy to a VPS, rather than a bog-standard Apache web host. I decided to deploy to DigitalOcean using Forge then continuosly deploy using Envoyer. The initial configuration of Forge + Envoyer was a bit confusing, but luckily Laracasts came to the rescue. After I got my head around how the two worked together, all updates I’ve done to the project since have been easily deployed with virtually no hassle at all.

Finally,

Testing… 1, 2, 3, testing… In Laravel 5.0.x, I was using Jeffrey Way’s Integrated testing utility, which got merged into Laravel core in 5.1. The aforementioned testing utility allows one to write tests in a more self-explanatory way. What that means is rather than getting down and dirty with PHPUnit, you can write tests like $this->visit('/')->see('Hello!'); which reads much better.

By the way, if you’re interested in signing up with DigitalOcean, please use my referral link. I scratch your back, you scratch mine 🙂