[PROJECT_NAME] and description
- Use Laravel Pint with the project's configuration (
pint.json) - Strict comparisons: Use
===and!==instead of==and!= - Strict parameters: Enable strict parameter checking
- Global namespace imports: Import classes, constants, and functions from global namespace
- Native function invocation: Use namespaced native function calls
- Method chaining indentation: Properly indent method chains
- Explicit string variables: Use explicit string variable syntax when appropriate
- Combine consecutive isset/unset: Group consecutive isset/unset calls
- Ternary to null coalescing: Prefer null coalescing operator (
??) over ternary
- PHPStan Level Max: All code must pass PHPStan at maximum level
- Type hints: Always provide type hints for:
- Method parameters
- Return types
- Properties (with PHPDoc when needed)
- Generic types: Use PHPDoc generics for complex types (e.g.,
@template TState) - Array shapes: Document array shapes with PHPDoc (e.g.,
array{enabled: bool, sampling: array{...}})
- Hooks are event listeners that capture Laravel framework events
- Located in
src/Hooks/ - Should be
finalclasses marked@internal - Always wrap hook logic in try-catch and report exceptions as handled
- Example pattern:
final class QueryExecutedListener
{
public function __construct(
private Core $nightwatch,
) {}
public function __invoke(QueryExecuted $event): void
{
try {
$this->nightwatch->query($event);
} catch (Throwable $e) {
$this->nightwatch->report($e, handled: true);
}
}
}- Sensors transform events into records and payload factories
- Located in
src/Sensors/ - Should be
finalclasses marked@internal - Return tuple:
[Record, callable(): array<mixed>](for record-based sensors) or?array(for direct array sensors likeScheduledTaskSensor) - Use
Locationhelper for file/line detection - Use
Clockfor timestamp management - Check property visibility: Before using reflection, verify if Laravel properties are public. Prefer direct property access over reflection when possible.
- Trust Laravel's internal constraints: If Laravel enforces constraints on a value (e.g., requires >= 1 second for repeating tasks), trust those constraints rather than re-checking in our code
- Use
RequestStatefor HTTP requests - Use
CommandStatefor Artisan commands, scheduled tasks, and job attempts - State is managed through
Coreclass - Execution stages tracked via
ExecutionStageenum
- Shared functionality goes in
src/Concerns/ - Examples:
CapturesState,RedactsRecords,RejectsRecords - Use traits to keep
Coreclass focused
- Final classes: Use
finalfor internal classes that shouldn't be extended - Visibility: Use
privateby default,protectedonly when necessary - Property promotion: Prefer constructor property promotion
- Readonly: Consider
readonlyproperties when appropriate (PHP 8.2+) - Avoid unnecessary variables: Only create variables when they're used multiple times or significantly improve readability. Don't create variables for single-use values that can be inlined directly.
- @internal: Mark all internal classes/methods with
@internal - @api: Mark public API methods with
@api - PHPDoc: Provide comprehensive PHPDoc for:
- Complex types
- Generic types
- Array shapes
- Callable signatures
- Method documentation: Document public methods, especially those marked
@api - Avoid unnecessary comments: Don't add inline comments unless necessary, especially when existing code doesn't already have them for similar scenarios. Let the code be self-documenting through clear naming and structure.
- Graceful degradation: Never let monitoring code break the application
- Exception handling: Always catch
Throwablein hooks - Report as handled: Internal exceptions should be reported as
handled: true - Unrecoverable exceptions: Use
Nightwatch::unrecoverableExceptionOccurred()for fatal errors
- Test coverage: Maintain high test coverage
- Test structure:
- Feature tests in
tests/Feature/ - Unit tests in
tests/Unit/
- Feature tests in
- Test helpers: Use
TestCasehelper methods:fakeIngest()for mocking ingestfakeTcpStreams()for testing networksetExecutionStart()for time manipulation
- Test isolation: Each test should be independent
- Test naming: Use descriptive test method names
- Laravel versions: Support Laravel 10, 11, and 12
- PHP versions: Minimum PHP 8.2
- Backward compatibility: Maintain backward compatibility within major versions
- Compatibility layer: Use
Compatibilityclass for version-specific code
Compatibility flags in the Compatibility class are used to handle version-specific features and APIs. Use them strategically:
When to use compatibility flags:
-
Events that don't fire in older versions
- Example:
$cacheDurationCapturable- start events (RetrievingKey,WritingKey, etc.) don't fire in older versions, so the sensor needs to know not to expect them for duration calculation - Example:
$terminatingEventExists- theTerminatingevent doesn't exist/fire in Laravel < 11.18.0 - Prevents the sensor from waiting for events that will never fire
- Example:
-
Classes/facades that don't exist in older versions
- Example:
$contextExists- theContextfacade doesn't exist in Laravel < 11 - Prevents fatal errors from accessing non-existent classes/methods
- Example:
-
Features added in specific versions that need conditional logic
- Example:
$subMinuteScheduledTasksSupported- sub-minute scheduling was added in Laravel 10.15.0 - Documents when features were introduced and provides explicit feature detection
- Example:
-
When fallback behavior is needed
- Example:
$queuedJobDurationCapturable- duration defaults to0if not capturable - Allows graceful degradation when features aren't available
- Example:
When NOT to use compatibility flags:
-
Properties that always exist but may be null
- Example:
$event->task->expression,$event->task->timezone- always exist, use null coalescing (??) if needed - No need for a flag if the property exists in all supported versions
- Example:
-
When null coalescing operator is sufficient
- Example:
$event->storeName ?? ''- property may not exist, but??handles it gracefully - Simpler than adding a compatibility flag for optional properties
- Example:
Guidelines:
- Document the feature: Always include PR link and version tag in the compatibility flag comment. You MUST verify both the PR link and version tag are correct using the process below - never guess or reuse PR numbers from other features.
- How to find the correct PR and version:
- Search GitHub:
curl -s "https://api.github.com/search/issues?q=repo:laravel/framework+<feature-keywords>+is:pr+is:merged" | grep -E '"title"|"number"|"html_url"' - Verify the PR title and number match the feature you're implementing
- Check the PR's merge date:
curl -s "https://api.github.com/repos/laravel/framework/pulls/<PR_NUMBER>" | grep "merged_at" - Find which release contains this PR:
curl -s "https://api.github.com/repos/laravel/framework/releases/tags/v<VERSION>" | grep "<PR_NUMBER>" - Verify the release notes explicitly mention the PR number
- Search GitHub:
- Common mistakes to avoid:
- Using similar PR numbers without verification (e.g., #47310 vs #47279)
- Guessing version numbers based on approximate dates
- Not checking if the PR actually exists or matches the feature
- Be explicit: Use flags to make intent clear (feature support vs. property existence)
- Consistency: Follow existing patterns in the codebase
- Performance: Flags are evaluated once at boot time, so they're efficient for runtime checks
- Test coverage: Ensure tests cover both supported and unsupported versions when using flags
- Sampling: Respect sampling configuration
- Buffering: Use
RecordsBufferfor efficient batching - Memory: Be mindful of memory usage, especially in long-running processes
- Redaction: Always redact sensitive data:
- Passwords
- Tokens
- Authorization headers
- Configurable via
redact_payload_fieldsandredact_headers
- Filtering: Respect filtering configuration
- No PII: Avoid capturing personally identifiable information unless explicitly configured
- Environment variables: Use
env()helper with defaults - Config file: Add new config options to
config/nightwatch.php - Dot notation: Use config dot notation for nested values
- Defaults: Provide sensible defaults
- Record classes: Located in
src/Records/ - Payload structure: Follow existing payload structure patterns
- Versioning: Use payload version (
'v' => 1) - Lazy values: Use
LazyValuefor values that cannot be determined at event capture time and need to be evaluated later when the record is serialized. This is for values that depend on state that will change or finalize between when the event is captured and when the record is sent to the ingest service - Grouping: Use
_grouphash for similar events. The_groupkey identifies the "type" or "identity" of an event, grouping events that are the same "thing" even if they're different executions. Include fields that define the identity of the event, not execution details or results. Guidelines:- Include: Fields that define WHAT the thing is or WHEN it's scheduled (e.g., task name, cron expression, route path, cache key, job class, query SQL)
- Exclude: Fields that define HOW it runs or WHAT happened (e.g., withoutOverlapping, duration, status, memory usage, response code)
- Identity vs. Execution: Identity fields define the "what" (e.g., route path for HTTP requests, cache key for cache operations, SQL for queries), while execution details define the "how" or results (e.g., middleware applied, cache hit/miss, query bindings)
- Consistency: Changing identity fields should create a new group; changing execution details should not. For example, changing a route path creates a new group, but changing response status does not
- Backward compatibility: When adding new fields to
_grouphashes, consider whether to include them conditionally to maintain existing group hashes. For example, only includerepeat_secondswhen it's non-zero to preserve hashes for regular scheduled tasks - Examples:
- ✅ Scheduled task:
{name},{expression},{timezone},{repeatSeconds}when repeatSeconds > 0, else{name},{expression},{timezone}- defines the schedule identity while preserving backward compatibility - ✅ HTTP request:
{methods},{domain},{route}- defines the route identity - ✅ Cache event:
{store},{key}- defines what's being cached - ❌ Don't include:
withoutOverlapping,onOneServer,duration,status,memoryUsage
- ✅ Scheduled task:
- String limits: Use
Str::tinyText(),Str::mediumText()for length limits - Field naming - ALWAYS check Laravel source code: Before creating any new field names, check Laravel's source code for the property names. Look at the actual class properties and methods in Laravel's framework code. Convert camelCase property names to snake_case for JSON payloads. Use Laravel's property names rather than creating new terminology.
- Complete records: All records must always contain all keys. Optional fields should use appropriate sentinel values rather than being omitted from the record. This ensures consistent record structure across all records, which is important for data consistency, easier querying, backward compatibility, and type safety.
- ClickHouse storage: Data is stored in ClickHouse, which doesn't typically use Nullable fields due to the extra storage required. Prefer using sentinel values instead of
nullwhenever possible. Choose sentinel values based on semantic validity:- Strings: Use empty strings (
'') when an empty string is not a meaningful value for the field - Numbers: Use
0when zero is semantically invalid for that field. Examples:- ✅
repeat_seconds => 0for non-repeating tasks (Laravel requires ≥1 second) - ✅
duration => 0for skipped tasks (they don't actually run) - ❌ Don't use
0if zero is a valid/meaningful value (e.g.,offset,retry_count)
- ✅
- Booleans: Use
falsewhen false is the appropriate default/absence state - Only use
nullwhen there is no appropriate sentinel value: Usenullonly when0/false/''would be ambiguous or when the absence of a value is semantically different from any sentinel value. For example:- ✅ Use
nullif both0and positive numbers are valid, and you need to distinguish "not set" - ✅ Use
nullif an empty string is a valid/meaningful value distinct from "not set"
- ✅ Use
- Check the existing codebase: Look for similar fields in other sensors to maintain consistency
- Strings: Use empty strings (
- Agent code: Located in
agent/directory - Build process: Use
build.shfor building PHAR - Docker: Agent has Docker support
- Scoping: Use
scoper.inc.phpfor PHAR scoping
- Branch: Work on
1.xbranch - Changelog: Update
CHANGELOG.mdfor user-facing changes - Version: Version managed in
version.txt - Commits: Write clear, descriptive commit messages
- Always run checks: After creating or editing files, always run:
- Tests: Run the test suite to ensure all tests pass
- Linting: Run Pint to ensure code formatting is correct
- Static analysis: Run PHPStan to ensure type safety and catch errors
- Before committing: Ensure all checks pass before committing code
- Automated checks: The CI pipeline will verify these, but catch issues locally first
- Verify links and PRs: When adding PR references or version tags (especially in compatibility flags), use the GitHub API or curl commands to verify:
- The PR number is correct and matches the feature
- The version tag actually contains that PR in its release notes
- Don't reuse PR numbers from other features or guess version numbers - always verify through GitHub's API or release notes
- Verify Laravel source: Before accessing Laravel properties or methods, check the actual Laravel source code to confirm:
- Property names and types
- Whether properties are public or require reflection
- What values Laravel provides (e.g.,
nullvs0) - Valid value ranges - Check for validation logic or constraints that indicate which values are semantically valid (e.g., if Laravel requires a value to be ≥1, then
0can be used as a sentinel for "not set") - When features were actually introduced (check changelogs, not just version tags)
- Match Laravel conventions: When adding new fields to payloads, always check Laravel's source code for the property names. Convert camelCase property names to snake_case for JSON payloads. Use Laravel's property names rather than creating new terminology. This ensures consistency with Laravel's own APIs and makes the data more familiar to Laravel developers.
- Group related fields: Place new fields logically alongside related fields in payloads (e.g., scheduling-related fields grouped together)
- Code passes PHPStan at max level
- Code formatted with Pint
- All tests pass
- New code has tests
- PHPDoc is complete
- Internal classes marked
@internal - Public API marked
@api - Error handling is graceful
- Sensitive data is redacted
- Performance considerations addressed
- Backward compatibility maintained
- Configuration options documented
// In Hook
try {
$this->nightwatch->query($event);
} catch (Throwable $e) {
$this->nightwatch->report($e, handled: true);
}// In Sensor
return [
$record = new Query(...),
function () use ($event, $record) {
$this->executionState->queries++;
return [
'v' => 1,
't' => 'query',
'timestamp' => $this->clock->microtime(),
// ... other fields
];
},
];// Update execution state
$this->executionState->stage = ExecutionStage::Action;
$this->executionState->currentExecutionStageStartedAtMicrotime = $this->clock->microtime();- ❌ Don't break backward compatibility
- ❌ Don't let monitoring code crash the application
- ❌ Don't capture sensitive data without redaction
- ❌ Don't skip error handling in hooks
- ❌ Don't use
publicproperties unless necessary - ❌ Don't forget to mark internal code with
@internal - ❌ Don't ignore PHPStan errors
- ❌ Don't commit code that doesn't pass Pint
- ❌ Don't add features without tests
- ❌ Don't use
dd()ordump()in production code
- ✅ Do use strict typing
- ✅ Do handle all exceptions gracefully
- ✅ Do write comprehensive tests
- ✅ Do document public APIs
- ✅ Do respect configuration options
- ✅ Do use final classes for internal code
- ✅ Do follow Laravel conventions
- ✅ Do maintain backward compatibility
- ✅ Do optimize for performance
- ✅ Do redact sensitive data