
シェア:
A trained actor with a dissertation on standup comedy, I came into PHP development via the meetup scene. You can find me speaking and writing on tech, or playing/buying odd records from my vinyl collection.
Monitor Your Webhooks with Laravel Nightwatch
所要時間:11 分
There are a ton of ways to monitor your application, and as a developer, it can be daunting. For PHP developers, you have the option of Blackfire and Tideways within the PHP space or external cloud platforms such as Datadog, Sentry, Papertrail or New Relic. The last four examples are Application Performance Monitoring, but all are third parties that concentrate on what their reporting agents do within the cloud. This doesn’t give us much insight into our code, unless, of course, you have written your enterprise or scale-up app code to log everything efficiently.
I have found setting up monitoring agents like this particularly painful and perhaps a little overly complex by necessity. This article aims to show you how Laravel took the majority of the complexity out, so we’re going to get Laravel Nightwatch up and running with some mock server request code to test it.
When you’re running production webhooks, it’s easy for things to go wrong; messages fail, payloads change, or responses slow down. As developers, we reach for monitoring tools to stay ahead of these issues, but the landscape can feel daunting. In the PHP world alone, you’ve got Blackfire and Tideways, and then the heavyweight APMs like Datadog, Sentry, Papertrail, or New Relic. These tools are powerful, but they’re third-party agents focused on cloud-side reporting, which doesn’t always give us the insight we want into what our Laravel code is actually doing.
And if we’re being honest, getting some of these agents set up is… painful. They work, but they often feel overly complex out of necessity.
Laravel Nightwatch, launched this year, aims to change that. It gives PHP developers a built-in, Laravel-native way to monitor application events and performance without the usual configuration headaches. In this tutorial, we’ll spin up a simple webhook receiver using Vonage RCS messages, send some mock traffic through it, and watch everything come to life in the Nightwatch dashboard.
Laravel has taken most of the heavy lifting out of app monitoring. Let’s see how far we can get with just a few steps.
TLDR; You can find the project code here.
Prerequisites
A Laravel Nightwatch account (steps included)
Set up the Laravel App
We’re building a monitoring application, but we need something to monitor! For this, we will build an app with RCS Messaging. RCS messaging is a shiny new protocol for sending SMS-like messages, but with far more capability. You can send and receive RCS messages using the Vonage Messages API and a Vonage Application, just like SMS. Where RCS really differs is in what it can deliver: higher-quality media (audio, images, video), larger file sizes, and richer interactive elements such as suggested replies, suggested actions, cards, and carousels.
What we’re going to mock is monitoring the incoming data when people reply; when an end user replies on their device, a functioning RCS application will be configured to fire Webhooks off to a designated URL to consume it.
The shortcut we can take here is that we don’t need to do all of that configuration: what I want to show is the Nightwatch Dashboard configured and receiving application data.
Using the Laravel installer (which wraps around Composer’s create-project), create a new Laravel application in your terminal:
laravel new tutorials-rcs-webhooks_laravel_nightwatchYou can ignore the scaffolding options, except that it is probably worth picking SQLite as the database since it’s easy to get off the ground.
To learn how to get started with SQLite, check out my previous article “The Return of SQLite”.
Once the process is complete, load your new Laravel application into the IDE of your choice. You’ll want the ability to view records in your database, so if you are using something like VSCode, have a SQL plugin, or if you’re using PHPStorm, set up the database source.
This application will have one entity, Webhook, which represents the model of the incoming data. What is the incoming data? It’s an RCS Webhook from Vonage (or mocking one). If you take a look at the OpenAPI spec for it, you can find a dummy example:
{
"channel": "rcs",
"message_uuid": "aaaaaaaa-bbbb-4ccc-8ddd-0123456789ab",
"to": "Vonage",
"from": "447700900001",
"timestamp": "2025-02-03T12:14:25Z",
"context_status": "none",
"message_type": "text",
"text": "Inbound message text."
} Make Models and Migrations
We will need a model and migration to create this, which you can do in one hit from the command line:
php artisan make:model Webhook --migrationWith the --migration switch, the command will create both the model and the migration. The world of AI has made things a little less laborious these days, so to get my migration code out, I threw this JSON into an AI agent and said “generate the migration code for this JSON”, which it promptly did (and surprisingly without mistakes!). Here’s what that migration looks like:
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('webhooks', function (Blueprint $table) {
$table->id();
$table->string('channel');
$table->uuid('message_uuid')->unique();
$table->string('to');
$table->string('from');
$table->timestamp('timestamp');
$table->string('context_status')->nullable();
$table->string('message_type')->nullable();
$table->text('text')->nullable();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('webhook_messages');
}
};So far, so good. That model is going to be hydrated by a controller, so to do this as quickly as possible, you’ll need to edit the model to make all of the properties fillable. Head to your app\Models\Webhook.php and edit the model to be like so:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Webhook extends Model
{
protected $fillable = [
'channel',
'message_uuid',
'to',
'from',
'timestamp',
'context_status',
'message_type',
'text',
];
protected $casts = [
'timestamp' => 'datetime',
];
}Excellent, the model can now be auto-filled with incoming data (and you don’t need to perform verification on the incoming data, because you trust Vonage, right? But seriously in production, always validate your inputs).
Make the Route and Controller
To handle the incoming data, we need a route tied to a Controller method. You could write this logic directly in the web.php route file as a closure, but in this tutorial, I’m going to make it a bit more traditional.
php artisan make:controller WebhookControllerI’ve used as few lines of code as possible in this controller to handle the payload:
<?php
namespace App\Http\Controllers;
use App\Models\Webhook;
use Illuminate\Http\Request;
class WebhookController extends Controller
{
public function handle(Request $request)
{
$data = $request->all();
Webhook::create($data);
return response('Webhook Handled', 200);
}
}In our routes, we will need to specify that the route is tied to the handle() method of the WebhookController. The handle() method does a single thing (perhaps the one time I have ever obeyed the single responsibility principle!), which is to take the request body, hydrate it into a Webhook entity, and persist it into the database.
Head to Routes\web.phpand add the route:
Route::post('/webhook', [WebhookController::class, 'handle']);Sending a request to this endpoint (you can try it if you like!) will not work currently. The route is there, the Controller is there, but it will only give a 419 response. This is because I have been a bad Laravel developer, but I can tell you how to be a good Laravel developer. I am trying to do this with as little logic as possible, so I didn’t scaffold out API boilerplate that is there if you want it. Because this route is in web.php, Laravel is configured to expect this to be GET and POST requests that compile HTML rather than real-time API REST JSON. So, it gives an HTTP 419 because it expects some sort of form request under a POST, and these have a Cross-Site Request Forgery token included.
The quick and dirty way to get around this is to hack the middleware (never do this in production!). I thought I’d include it to show a bit about what happens at runtime with Laravel under the hood. Head to your framework bootstrap file, located in bootstrap\app.php.
This file serves as the opening configuration point for your application, just after the entry point. You can configure custom routing files, custom error handlers at the global level, and custom middleware. In my years of Laravel development, I would say it’s probably a bad idea to edit anything at the global level like this, but I’m also keen to get our request completed.
bootstrap\app.php
use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Foundation\Configuration\Middleware;
return Application::configure(basePath: dirname(__DIR__))
->withRouting(
web: DIR.'/../routes/web.php',
commands: DIR.'/../routes/console.php',
health: '/up',
)
->withMiddleware(function (Middleware $middleware): void {
$middleware->validateCsrfTokens(except: [
'/webhook',
]);
})
->withExceptions(function (Exceptions $exceptions): void {
//
})->create();You can see that withMiddleware() uses the specific method validateCsrfTokens() to stop any cross-site forgery. Handy if you want to bypass the rules!
A Mock NodeJS Server
In the real world, the whole point of using something like Nighwatch is application performance monitoring, which by necessity means you are likely to be dealing with a lot a asynchronous jobs on the queue, a lot of caching, a lot of logging, and a lot of requests. It’s time to fake-it-till-we-make-it, and I took the easiest route to show a dashboard filling up: requests.
There is only one type of incoming request, and that is the Webhook. Therefore, we need some code or a server to generate these requests to fire at our application for us. There are all sorts of API tooling out there for this type of thing: Prisma, Postman, Insomnia, Wiremock, you name it. To quote a philosophy of mine, I opted to “keep it simple, stupid!”. One JavaScript file, acting in a loop as a server.
In the root of your project, make a directory and name it accordingly:
mkdir mock_server
cd mock_server
npm init -y
touch mock-webhook-server.jsThese four commands make the directory, add dependency management, and create the file we’re going to run as the server. There are three things we need to send these requests on a loop:
Axios, for sending AJAX requests
UUID, to generate unique dummy IDs for the dummy webhooks
Faker, a library to generate phone and text dummy data.
To install all at once, enter this into your command line:
npm i axios uuid @faker-js/fakerHere is the completed server code:
const axios = require('axios');
const { v4: uuidv4 } = require('uuid');
const { faker } = require('@faker-js/faker');
const ERROR_RATE = 0.025;
const ERROR_FIELDS = ['message_uuid', 'from', 'timestamp', 'text'];
function generateWebhookPayload() {
const payload = {
channel: "rcs",
message_uuid: uuidv4(),
to: "Vonage",
from: faker.phone.number({ style: 'international' }).replace('+', ''),
timestamp: new Date().toISOString(),
context_status: "none",
message_type: "text",
text: faker.lorem.text().slice(0, 300)
};
if (Math.random() < ERROR_RATE) {
const fieldToRemove = faker.helpers.arrayElement(ERROR_FIELDS);
delete payload[fieldToRemove];
console.warn[WARN] Sending malformed webhook (missing '${fieldToRemove}'));
}
return payload;
}
function sendWebhook() {
const payload = generateWebhookPayload();
axios.post('http://tutorial-voice-rcs_laravel_nightwatch.test/webhook', payload)
.then(() => {
console.log[${new Date().toISOString()}] Sent: ${payload.message_uuid});
})
.catch(error => {
console.error[${new Date().toISOString()}] Error: ${error.message});
});
}
setInterval(sendWebhook, 200);
console.log("Running Mock Webhook Server"); Unpacking the Server Code
We have two constants, ERROR_RATE and ERROR_FIELDS. The first is a decimal to state the probability of an error when sending requests, and the second is which fields will be removed from the JSON sent that will cause the error.
generateWebhookPayload() is the method to shape the JSON payload that will also decide whether the payload is valid or not. After all, never trust someone else’s code, right? This is the point where we invoke the helper function uuidv4() to generate the UUID, and faker to generate some dummy text that is up to 300 characters long (this is arbitrary, RCS messages can have up to 3000 characters).
Our clever little mathematical line if (Math.random() < ERROR_RATE) { works out whether it’s going to remove one of the required fields. After all, Nightwatch is for visualing problem points within your application.
sendWebhook() is the method called in an event loop, which calls to make the payload data and then sends it to our application. This is done just below the logic of the method, in setInterval(sendWebhook, 200) // <- set this according to how quick you want to generate dummy data
You can now run it, but remember your SQLite database is going to fill up fast. It’s time for Nightwatch.
Setting Up Nightwatch
The beauty of Nightwatch is that it’s free to tinker around with. The free tier comes with up to 300k application events being recorded in the cloud platform, so easily enough to play with.
Head to the Nightwatch Site and create an account and an application. The four steps are pretty well documented on there. Firstly, name your application and choose a storage region appropriate to your location.
Setting up your Nightwatch applicationNext are the parts that really take the headache out of setting up an APM. You are instructed to first install Nightwatch via Composer:
composer require laravel/nightwatchYou are given a set of keys to put into your environment variable file (these are dummy keys, for the security hawkeyes out there):
Everything you need to know to get your agent reportingStep three stops your Nightwatch account from coming to its quota within the first hour: remember, you might be monitoring potentially millions of events here, so Nightwatch comes with the ability to define your sample size as an environment variable:
How much data do you want?Finally, you boot up Nightwatch in your console locally, and it sends the sample rate to the Nightwatch servers:
Run your agent through php artisan, same as everything else
In just four steps, you have an APM. Run your Mock Node Webhook Server and in comes the data:
Elegant application monitoring is here!Somehow, I even managed to create a rogue request that hogged CPU time, which you can see as a spike in the performance graph on the right. You’re presented with a ton of features to drill down more into your application’s data: SQL violations, memory exceptions, incomplete jobs, you name it. As I was randomly generating exceptions, I had to look to see what those looked like:
Watch out for your exception ratesNice. It splits it into handled vs. unhandled as well, so you get a better view of how defensive your application’s code is performing.
Conclusion
Each year, developers seem to be gifted another part of the Laravel application stack that hasn’t been covered yet, and this is no exception. What makes Nightwatch stand out is that 3rd party vendors of Application monitoring and performance tooling aren’t tailored to Laravel, as opposed to something by Laravel themselves. In this regard, for consumers of large amounts of data (using the Vonage Verify, Messages or Voice API has event webhook systems that will generate large amounts of webhooks), this is such an easy win that it’s hard to ignore.
Have a question or something to share? Join the conversation on the Vonage Community Slack, stay up to date with the Developer Newsletter, follow us on X (formerly Twitter), subscribe to our YouTube channel for video tutorials, and follow the Vonage Developer page on LinkedIn, a space for developers to learn and connect with the community. Stay connected, share your progress, and keep up with the latest developer news, tips, and events!


