Performance tracing with sentry

This commit is contained in:
David Bomba 2025-04-10 15:41:59 +10:00
parent 7b265c8133
commit 5bf4bde9f0
6 changed files with 274 additions and 18 deletions

View File

@ -214,7 +214,7 @@ class CreditController extends BaseController
if ($credit->invoice_id) { if ($credit->invoice_id) {
$credit = $credit->service()->markSent()->save(); $credit = $credit->service()->markSent()->save();
// $credit->client->service()->updatePaidToDate(-1 * $credit->balance)->save(); // If we mutate the paid to date, we need to reverse the status of the invoice, this will allow the credit note that has been created to be used and double paid to dates prevented. // $credit->client->service()->updatePaidToDate(-1 * $credit->balance)->save(); // If we mutate the paid to date, we need to reverse the status of the invoice, this will allow the credit note that has been created to be used and double paid to dates prevented.
$credit->client->service()->updateBalanceAndPaidToDate(-1 * $credit->balance, -1 * $credit->balance)->save(); $credit->client->service()->updateBalanceAndPaidToDate(-1 * ($credit->invoice->balance ?? 0), -1 * $credit->balance)->save();
// $invoice = $credit->invoice; // $invoice = $credit->invoice;
$invoice = \App\Models\Invoice::withTrashed()->find($credit->invoice_id); $invoice = \App\Models\Invoice::withTrashed()->find($credit->invoice_id);

View File

@ -80,6 +80,7 @@ class ValidInvoiceCreditRule implements Rule
$cost = 0; $cost = 0;
foreach (request()->input('line_items') as $item) { foreach (request()->input('line_items') as $item) {
$item = (array)$item;
$cost += $item['cost'] * $item['quantity']; $cost += $item['cost'] * $item['quantity'];
} }

View File

@ -228,7 +228,7 @@ class TemplateAction implements ShouldQueue
public function middleware() public function middleware()
{ {
return [new WithoutOverlapping("template-{$this->company->company_key}")]; return [(new WithoutOverlapping('template-' . $this->company->company_key . $this->entity))->releaseAfter(60)];
} }
} }

View File

@ -621,6 +621,7 @@ class TemplateService
'payments' => $payments, 'payments' => $payments,
'total_tax_map' => $invoice->calc()->getTotalTaxMap(), 'total_tax_map' => $invoice->calc()->getTotalTaxMap(),
'line_tax_map' => $invoice->calc()->getTaxMap()->toArray(), 'line_tax_map' => $invoice->calc()->getTaxMap()->toArray(),
'project' => $invoice->project ? $this->transformProject($invoice->project, true) : [],
]; ];
}); });
@ -1172,7 +1173,7 @@ class TemplateService
'client' => $this->getClient($project), 'client' => $this->getClient($project),
'user' => $this->userInfo($project->user), 'user' => $this->userInfo($project->user),
'assigned_user' => $project->assigned_user ? $this->userInfo($project->assigned_user) : [], 'assigned_user' => $project->assigned_user ? $this->userInfo($project->assigned_user) : [],
'invoices' => $this->processInvoices($project->invoices), 'invoices' => !$nested ? $this->processInvoices($project->invoices) : [],
'expenses' => ($project->expenses && !$nested) ? $this->processExpenses($project->expenses, true) : [], 'expenses' => ($project->expenses && !$nested) ? $this->processExpenses($project->expenses, true) : [],
]; ];

View File

@ -1,29 +1,129 @@
<?php <?php
/**
* Sentry Laravel SDK configuration file.
*
* @see https://docs.sentry.io/platforms/php/guides/laravel/configuration/options/
*/
return [ return [
//'dsn' => env('SENTRY_LARAVEL_DSN', env('SENTRY_DSN')), // @see https://docs.sentry.io/product/sentry-basics/dsn-explainer/
'dsn' => config('ninja.sentry_dsn'), 'dsn' => config('ninja.sentry_dsn'),
// @see https://spotlightjs.com/
// 'spotlight' => env('SENTRY_SPOTLIGHT', false),
// @see: https://docs.sentry.io/platforms/php/guides/laravel/configuration/options/#logger
// 'logger' => Sentry\Logger\DebugFileLogger::class, // By default this will log to `storage_path('logs/sentry.log')`
// The release version of your application
// Example with dynamic git hash: trim(exec('git --git-dir ' . base_path('.git') . ' log --pretty="%h" -n1 HEAD'))
'release' => config('ninja.app_version'), 'release' => config('ninja.app_version'),
'breadcrumbs' => [ // When left empty or `null` the Laravel environment will be used (usually discovered from `APP_ENV` in your `.env`)
// Capture Laravel logs in breadcrumbs 'environment' => env('SENTRY_ENVIRONMENT'),
'logs' => true,
// Capture SQL queries in breadcrumbs // @see: https://docs.sentry.io/platforms/php/guides/laravel/configuration/options/#sample-rate
'sql_queries' => true, 'sample_rate' => env('SENTRY_SAMPLE_RATE') === null ? 1 : (float) env('SENTRY_SAMPLE_RATE'),
// Capture bindings on SQL queries logged in breadcrumbs // @see: https://docs.sentry.io/platforms/php/guides/laravel/configuration/options/#traces-sample-rate
'sql_bindings' => true, 'traces_sample_rate' => env('SENTRY_TRACES_SAMPLE_RATE') === null ? 0.01 : (float) env('SENTRY_TRACES_SAMPLE_RATE'),
// Capture queue job information in breadcrumbs // @see: https://docs.sentry.io/platforms/php/guides/laravel/configuration/options/#profiles-sample-rate
'queue_info' => true, 'profiles_sample_rate' => env('SENTRY_PROFILES_SAMPLE_RATE') === null ? 0.001 : (float) env('SENTRY_PROFILES_SAMPLE_RATE'),
// Capture command information in breadcrumbs // @see: https://docs.sentry.io/platforms/php/guides/laravel/configuration/options/#send-default-pii
'command_info' => true, 'send_default_pii' => env('SENTRY_SEND_DEFAULT_PII', false),
// @see: https://docs.sentry.io/platforms/php/guides/laravel/configuration/options/#ignore-exceptions
// 'ignore_exceptions' => [],
// @see: https://docs.sentry.io/platforms/php/guides/laravel/configuration/options/#ignore-transactions
'ignore_transactions' => [
// Ignore Laravel's default health URL
'/up',
],
// Breadcrumb specific configuration
'breadcrumbs' => [
// Capture Laravel logs as breadcrumbs
'logs' => env('SENTRY_BREADCRUMBS_LOGS_ENABLED', true),
// Capture Laravel cache events (hits, writes etc.) as breadcrumbs
'cache' => env('SENTRY_BREADCRUMBS_CACHE_ENABLED', true),
// Capture Livewire components like routes as breadcrumbs
'livewire' => env('SENTRY_BREADCRUMBS_LIVEWIRE_ENABLED', true),
// Capture SQL queries as breadcrumbs
'sql_queries' => env('SENTRY_BREADCRUMBS_SQL_QUERIES_ENABLED', true),
// Capture SQL query bindings (parameters) in SQL query breadcrumbs
'sql_bindings' => env('SENTRY_BREADCRUMBS_SQL_BINDINGS_ENABLED', false),
// Capture queue job information as breadcrumbs
'queue_info' => env('SENTRY_BREADCRUMBS_QUEUE_INFO_ENABLED', true),
// Capture command information as breadcrumbs
'command_info' => env('SENTRY_BREADCRUMBS_COMMAND_JOBS_ENABLED', true),
// Capture HTTP client request information as breadcrumbs
'http_client_requests' => env('SENTRY_BREADCRUMBS_HTTP_CLIENT_REQUESTS_ENABLED', true),
// Capture send notifications as breadcrumbs
'notifications' => env('SENTRY_BREADCRUMBS_NOTIFICATIONS_ENABLED', true),
],
// Performance monitoring specific configuration
'tracing' => [
// Trace queue jobs as their own transactions (this enables tracing for queue jobs)
'queue_job_transactions' => env('SENTRY_TRACE_QUEUE_ENABLED', true),
// Capture queue jobs as spans when executed on the sync driver
'queue_jobs' => env('SENTRY_TRACE_QUEUE_JOBS_ENABLED', true),
// Capture SQL queries as spans
'sql_queries' => env('SENTRY_TRACE_SQL_QUERIES_ENABLED', true),
// Capture SQL query bindings (parameters) in SQL query spans
'sql_bindings' => env('SENTRY_TRACE_SQL_BINDINGS_ENABLED', false),
// Capture where the SQL query originated from on the SQL query spans
'sql_origin' => env('SENTRY_TRACE_SQL_ORIGIN_ENABLED', true),
// Define a threshold in milliseconds for SQL queries to resolve their origin
'sql_origin_threshold_ms' => env('SENTRY_TRACE_SQL_ORIGIN_THRESHOLD_MS', 100),
// Capture views rendered as spans
'views' => env('SENTRY_TRACE_VIEWS_ENABLED', true),
// Capture Livewire components as spans
'livewire' => env('SENTRY_TRACE_LIVEWIRE_ENABLED', true),
// Capture HTTP client requests as spans
'http_client_requests' => env('SENTRY_TRACE_HTTP_CLIENT_REQUESTS_ENABLED', true),
// Capture Laravel cache events (hits, writes etc.) as spans
'cache' => env('SENTRY_TRACE_CACHE_ENABLED', true),
// Capture Redis operations as spans (this enables Redis events in Laravel)
'redis_commands' => env('SENTRY_TRACE_REDIS_COMMANDS', false),
// Capture where the Redis command originated from on the Redis command spans
'redis_origin' => env('SENTRY_TRACE_REDIS_ORIGIN_ENABLED', true),
// Capture send notifications as spans
'notifications' => env('SENTRY_TRACE_NOTIFICATIONS_ENABLED', true),
// Enable tracing for requests without a matching route (404's)
'missing_routes' => env('SENTRY_TRACE_MISSING_ROUTES_ENABLED', false),
// Configures if the performance trace should continue after the response has been sent to the user until the application terminates
// This is required to capture any spans that are created after the response has been sent like queue jobs dispatched using `dispatch(...)->afterResponse()` for example
'continue_after_response' => env('SENTRY_TRACE_CONTINUE_AFTER_RESPONSE', true),
// Enable the tracing integrations supplied by Sentry (recommended)
'default_integrations' => env('SENTRY_TRACE_DEFAULT_INTEGRATIONS_ENABLED', true),
], ],
// @see: https://docs.sentry.io/error-reporting/configuration/?platform=php#send-default-pii
'send_default_pii' => false,
]; ];

View File

@ -43,6 +43,160 @@ class CreditTest extends TestCase
$this->makeTestData(); $this->makeTestData();
} }
public function testCreditReversalScenarioInvoicePartiallyPaid()
{
$c = Client::factory()->create([
'company_id' => $this->company->id,
'user_id' => $this->user->id,
'balance' => 0,
'paid_to_date' => 0,
]);
$ii = new InvoiceItem();
$ii->cost = 100;
$ii->quantity = 1;
$ii->product_key = 'xx';
$ii->notes = 'yy';
$i = \App\Models\Invoice::factory()->create([
'company_id' => $this->company->id,
'user_id' => $this->user->id,
'client_id' => $c->id,
'tax_name1' => '',
'tax_name2' => '',
'tax_name3' => '',
'tax_rate1' => 0,
'tax_rate2' => 0,
'tax_rate3' => 0,
'discount' => 0,
'line_items' => [
$ii
],
'status_id' => 1,
]);
$i = $i->calc()->getInvoice();
$i = $i->service()->markSent()->save();
$this->assertEquals(100, $i->balance);
$this->assertEquals(100, $i->amount);
$i->service()->applyPaymentAmount(50, 'test');
$i->refresh();
$this->assertEquals(50, $i->balance);
$this->assertEquals(100, $i->amount);
$credit_array = $i->withoutRelations()->toArray();
$credit_array['invoice_id'] = $i->hashed_id;
$credit_array['client_id'] = $c->hashed_id;
$ii = new InvoiceItem();
$ii->cost = 50;
$ii->quantity = 1;
$ii->product_key = 'xx';
$ii->notes = 'yy';
$credit_array['line_items'] = [$ii];
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->post('/api/v1/credits', $credit_array);
$response->assertStatus(200);
$arr = $response->json();
$this->assertEquals(50, $arr['data']['balance']);
$this->assertEquals(50, $arr['data']['amount']);
$this->assertEquals(2, $arr['data']['status_id']);
$i->refresh();
$c->refresh();
$this->assertEquals(0, $c->balance);
$this->assertEquals(6, $i->status_id);
}
public function testCreditReversalScenarioInvoicePaidInFull()
{
$c = Client::factory()->create([
'company_id' => $this->company->id,
'user_id' => $this->user->id,
'balance' => 0,
'paid_to_date' => 0,
]);
$ii = new InvoiceItem();
$ii->cost = 100;
$ii->quantity = 1;
$ii->product_key = 'xx';
$ii->notes = 'yy';
$i = \App\Models\Invoice::factory()->create([
'company_id' => $this->company->id,
'user_id' => $this->user->id,
'client_id' => $c->id,
'tax_name1' => '',
'tax_name2' => '',
'tax_name3' => '',
'tax_rate1' => 0,
'tax_rate2' => 0,
'tax_rate3' => 0,
'discount' => 0,
'line_items' => [
$ii
],
'status_id' => 1,
]);
$i = $i->calc()->getInvoice();
$i = $i->service()->markSent()->save();
$this->assertEquals(100, $i->balance);
$this->assertEquals(100, $i->amount);
$i->service()->applyPaymentAmount(100, 'test');
$i->refresh();
$this->assertEquals(0, $i->balance);
$this->assertEquals(100, $i->amount);
$this->assertEquals(4, $i->status_id);
$credit_array = $i->withoutRelations()->toArray();
$credit_array['invoice_id'] = $i->hashed_id;
$credit_array['client_id'] = $c->hashed_id;
$ii = new InvoiceItem();
$ii->cost = 100;
$ii->quantity = 1;
$ii->product_key = 'xx';
$ii->notes = 'yy';
$credit_array['line_items'] = [$ii];
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->post('/api/v1/credits', $credit_array);
$response->assertStatus(200);
$arr = $response->json();
$this->assertEquals(100, $arr['data']['balance']);
$this->assertEquals(100, $arr['data']['amount']);
$this->assertEquals(2, $arr['data']['status_id']);
$i->refresh();
$c->refresh();
$this->assertEquals(0, $c->balance);
$this->assertEquals(6, $i->status_id);
}
public function testPaidToDateAdjustments() public function testPaidToDateAdjustments()
{ {