feat(ai): add embeddings instrumentation and CI compatibility#1111
feat(ai): add embeddings instrumentation and CI compatibility#1111constantinius wants to merge 18 commits intoconstantinius/feat/ai/integration-basefrom
Conversation
Add the initial AI integration wiring that records invoke_agent spans for prompting and prompted lifecycle events, including model/provider and token usage metadata. This establishes the stack base without chat, tool, message, or embeddings instrumentation. Made-with: Cursor
| } | ||
|
|
||
| foreach ($source->toolCalls as $toolCall) { | ||
| $parts[] = $this->buildToolCallPart($toolCall); |
There was a problem hiding this comment.
Bug: The buildOutputMessages method iterates over $source->toolCalls and $source->toolResults without checking if they are null, which can cause a TypeError.
Severity: CRITICAL
Suggested Fix
Add null-coalescing operators to the foreach loops to ensure an empty array is used as a default value if the properties are null. For example: foreach (($source->toolCalls ?? []) as $toolCall) and foreach (($source->toolResults ?? []) as $toolResult).
Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent.
Verify if this is a real issue. If it is, propose a fix; if not, explain why it's not
valid.
Location: src/Sentry/Laravel/Features/AiIntegration.php#L738
Potential issue: In the `buildOutputMessages` method, the properties
`$source->toolCalls` and `$source->toolResults` are iterated over using `foreach`
without a prior null check. These properties are declared on their respective objects
but are not initialized in the constructor, meaning they can be `null`. If the Laravel
AI library returns a response or step object where these properties are not explicitly
set, attempting to iterate over `null` will cause a `TypeError` in PHP 8, leading to a
runtime crash.
Did we get this right? 👍 / 👎 to inform future reviews.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
Autofix Details
Bugbot Autofix prepared fixes for both issues found in the latest run.
- ✅ Fixed: CI runs AI tests on PHP versions lacking required dependencies
- The CI AI test step now only runs for Laravel 12 on PHP 8.4/8.5, matching the conditional laravel/ai installation guard.
- ✅ Fixed: Nullable
$invocationIdrisks undefined array key access- The method contract was tightened by changing
setConversationIdOnSpansto require a non-nullstringinvocation ID before indexing the invocation map.
- The method contract was tightened by changing
Or push these changes by commenting:
@cursor push 026cff2827
Preview (026cff2827)
diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -79,7 +79,7 @@
run: vendor/bin/phpunit --coverage-clover=coverage.xml --coverage-filter=src/Sentry
- name: Run AI integration tests
- if: matrix.packages.laravel == '^12.0'
+ if: matrix.packages.laravel == '^12.0' && (matrix.php == '8.4' || matrix.php == '8.5')
run: vendor/bin/phpunit test/Sentry/Features/AiIntegrationTest.php
- name: Upload coverage to Codecov
diff --git a/src/Sentry/Laravel/Features/AiIntegration.php b/src/Sentry/Laravel/Features/AiIntegration.php
--- a/src/Sentry/Laravel/Features/AiIntegration.php
+++ b/src/Sentry/Laravel/Features/AiIntegration.php
@@ -573,7 +573,7 @@
}
}
- private function setConversationIdOnSpans(?string $invocationId, ?string $conversationId): void
+ private function setConversationIdOnSpans(string $invocationId, ?string $conversationId): void
{
if ($conversationId !== null) {
$invocation = $this->invocations[$invocationId];This Bugbot Autofix run was free. To enable autofix for future PRs, go to the Cursor dashboard.
| $span->setData($data); | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
Nullable $invocationId risks undefined array key access
Low Severity
setConversationIdOnSpans declares $invocationId as ?string but accesses $this->invocations[$invocationId] without a null guard when $conversationId is non-null. If $invocationId is ever null, this produces an undefined array key warning. The parameter type hints nullable but the method body assumes non-null — it likely just needs to be typed string.
Replace inline stub definitions for Laravel AI interfaces, classes, events, attributes, and file types with the real implementations from the laravel/ai package added as a dev dependency. Drop PHP version and Laravel version skip guards from setUp() since these tests only run on PHP 8.4+. Simplify TestAgentWithConfig to use native PHP 8 attributes directly instead of the eval() workaround for older PHP versions.
6bfe4a7 to
7cc0d90
Compare
| } | ||
| $entryBytes = \strlen($inputJson) + (empty($kept) ? 0 : 1); // +1 for comma separator | ||
|
|
There was a problem hiding this comment.
Bug: The truncateEmbeddingInputs function iterates backward and breaks early, potentially discarding smaller inputs that would fit within the budget, leading to unnecessary data loss.
Severity: HIGH
Suggested Fix
Modify the truncateEmbeddingInputs function to not break when an item is too large. Instead, it should continue to the next item in the backward iteration. This will allow it to check earlier, potentially smaller items in the array and include them if they fit within the budget, maximizing data retention.
Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent.
Verify if this is a real issue. If it is, propose a fix; if not, explain why it's not
valid.
Location: src/Sentry/Laravel/Features/AiIntegration.php#L1063-L1065
Potential issue: The `truncateEmbeddingInputs` function has incorrect logic for
truncating inputs based on a byte budget. It iterates backward from the end of the input
array and stops processing entirely (`break`) as soon as it encounters an item that is
too large to fit in the remaining budget. This prevents the function from considering
smaller items that may appear earlier in the array and would fit. This can lead to
significant and silent data loss by underutilizing the available budget, as inputs that
should have been included are unnecessarily discarded.
Remove laravel/ai globally in both phpunit and phpunit-legacy jobs since it requires PHP 8.4+ and only supports Laravel 12. Re-add it conditionally for the supported combination (PHP 8.4+, Laravel 12).
7cc0d90 to
f432815
Compare
|
|
||
| array_unshift($kept, $inputs[$i]); | ||
| $totalBytes += $entryBytes; | ||
| } |
There was a problem hiding this comment.
Embeddings truncation keeps last inputs instead of first
Medium Severity
truncateEmbeddingInputs iterates backwards from the end of the $inputs array, prioritizing keeping the last inputs and dropping the first when the byte budget is exceeded. The Python SDK equivalent keeps the first inputs (earliest in the list). Additionally, the fallback on line 1075 uses end($inputs) to grab the last input, which is consistent with the backwards logic but inconsistent with the expected behavior of preserving the beginning of the input list. This means when truncation kicks in, the most important (first-provided) embedding inputs are silently dropped.
Additional Locations (1)
The Laravel AI classes are not available when PHPStan runs on PHP 8.3 without laravel/ai installed. Add baseline ignores matching the existing pattern used for other optional packages (Sanctum, Livewire, Lumen).
Capture provider HTTP request/response events as gen_ai.chat spans under invoke_agent and enrich each chat span with response model, usage, finish reason, and conversation id mapping. This keeps tool/message/embeddings behavior out of scope for follow-up stacked branches. Made-with: Cursor
Add execute_tool span lifecycle around InvokingTool and ToolInvoked events, and propagate serialized tool definitions onto invoke_agent and chat spans. This isolates tool-level monitoring while leaving message payload capture and embeddings for later stacked branches. Made-with: Cursor
Add a HasTools stub and implement it on TestAgent so tool definition metadata is populated under the same contract checks used by AiIntegration. Made-with: Cursor
Add structured input/output message capture for invoke_agent and chat spans, including attachment transformation, payload truncation, and binary redaction safeguards. This layers privacy-aware message instrumentation on top of existing invoke/chat/tool tracing without introducing embeddings yet. Made-with: Cursor
Pass attachments as an array to match Laravel AI AgentPrompt constructor typing on newer framework versions. Made-with: Cursor
Introduce embeddings span tracing and complete the remaining AI integration plumbing, including laravel/ai dependency, CI matrix handling, and phpunit configuration so AI tests run only in compatible environments. This final stacked branch brings the feature set to parity with the original large PR. Made-with: Cursor
f432815 to
1dd7a83
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
There are 3 total unresolved issues (including 2 from previous reviews).
Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
| $inputMessages = $this->buildChatInputMessages($steps, $index); | ||
| } else { | ||
| // Streaming without steps: use response output as input for subsequent chat spans | ||
| $inputMessages = $this->buildOutputMessages($response); |
There was a problem hiding this comment.
Empty Collection check always evaluates true, dead streaming fallback
Medium Severity
$steps obtained from $response->steps ?? [] is typically a Laravel Collection. PHP's empty() on any object — including an empty Collection — always returns false. So !empty($steps) at line 553 is always true when $steps is a Collection, making the streaming fallback branch (lines 555–557) unreachable dead code. For streaming responses with multiple chat spans and PII enabled, subsequent spans get empty input messages instead of the intended response output. Using count($steps) > 0 or $steps->isNotEmpty() would fix this.



Introduce embeddings span tracing and complete the remaining AI integration plumbing, including laravel/ai dependency, CI matrix handling, and phpunit configuration so AI tests run only in compatible environments. This final stacked branch brings the feature set to parity with the original large PR.
Summary
Finalizes the AI integration stack by adding embeddings tracing and the remaining compatibility/CI plumbing needed for matrix stability.
Included
GeneratingEmbeddings-> startgen_ai.embeddingsspanEmbeddingsGenerated-> finish/enrich embeddings spantracing.gen_ai_embeddingstoggle.composer.json(laravel/aidev dependency).github/workflows/ci.yaml.github/workflows/cs.yamlphpunit.xmlMade-with: Cursor
Issues