Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
129 changes: 129 additions & 0 deletions .claude/skills/pr-review/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
---
name: pr-review
description: Review code changes on the current branch for quality, bugs, performance, and security
disable-model-invocation: true
argument-hint: "[optional: LINEAR-TICKET-ID]"
allowed-tools: Read, Grep, Glob, Bash(git diff:*), Bash(git log:*), Bash(git show:*), Bash(git branch:*), Bash(gh pr:*), Bash(gh api:*), Bash(~/.claude/scripts/fetch-github-pr.sh:*), Bash(~/.claude/scripts/fetch-sentry-data.sh:*), Bash(~/.claude/scripts/fetch-slack-thread.sh:*)
---

# Code Review

You are reviewing code changes on the current branch. Your review must be based on the **current state of the code right now**, not on anything you've seen earlier in this conversation.

## CRITICAL: Always Use Fresh Data

**IGNORE any file contents, diffs, or line numbers you may have seen earlier in this conversation.** They may be stale. You MUST re-fetch everything from scratch using the commands below.

## Step 1: Get the Current Diff and PR Context

Run ALL of these commands to get a fresh view:

```bash
# The authoritative diff -- only review what's in HERE
git diff main...HEAD

# Recent commits on this branch
git log --oneline main..HEAD

# PR description and comments
gh pr view --json number,title,body,comments,reviews,reviewRequests
```

Also fetch PR review comments (inline code comments):

```bash
# Get the PR number
PR_NUMBER=$(gh pr view --json number -q '.number')

# Fetch all review comments (inline comments on specific lines)
gh api repos/{owner}/{repo}/pulls/$PR_NUMBER/comments --jq '.[] | {path: .path, line: .line, body: .body, user: .user.login, created_at: .created_at}'

# Fetch review-level comments (general review comments)
gh api repos/{owner}/{repo}/pulls/$PR_NUMBER/reviews --jq '.[] | {state: .state, body: .body, user: .user.login}'
```

## Step 2: Understand Context from PR Comments

Before reviewing, read through the PR comments and review comments. Note **who** said what (by username).

- **Already-addressed feedback**: If a reviewer pointed out an issue and the author has already fixed it (the fix is visible in the current diff), do NOT re-raise it.
- **Ongoing discussions**: Note any unresolved threads -- your review should take these into account.
- **Previous approvals/requests for changes**: Understand what reviewers have already looked at.

**IMPORTANT**: Your review is YOUR independent review. Do not take credit for or reference other reviewers' findings as if they were yours. If another reviewer already flagged something, you can note "as [reviewer] pointed out" but do not present their feedback as your own prior review. Your verdict should be based solely on your own analysis of the current code.

## Step 3: Get Requirements Context

Check if a Linear ticket ID was provided as an argument ($ARGUMENTS). If not, try to extract it from the branch name (pattern: `{username}/{linear-ticket}-{title}`).

If a Linear ticket is found:
- Use Linear MCP tools (`get_issue`) to get the issue details and comments
- **Check for a parent ticket**: If the issue has a parent issue, fetch the parent too. Our pattern is to have a parent ticket with project-wide requirements and sub-tickets for specific tasks (often one per repo/PR). The parent ticket will contain the full scope of the project, while the sub-ticket scopes what this specific PR should cover. Use both to assess completeness — the PR should fulfill the sub-ticket's scope, and that scope should be a reasonable subset of the parent's backend-related requirements.
- Look for Sentry links in the description/comments; if found, use Sentry MCP tools to get error details
- Assess whether the changes fulfill the ticket requirements

If no ticket is found, check the PR description for context on what the changes are meant to accomplish.

## Step 4: Review the Code

Review ONLY the changed lines (from `git diff main...HEAD`). Do not comment on unchanged code.

**When referencing code, always use the file path and quote the actual code snippet** rather than citing line numbers, since line numbers shift as the branch evolves.

### Code Quality
- Is the code well-structured and maintainable?
- Does it follow CLAUDE.md conventions? (import grouping, error handling with lib/errors, naming, alphabetization, etc.)
- Any AI-generated slop? (excessive comments, unnecessary abstractions, over-engineering)

### Performance
- N+1 queries, inefficient loops, missing indexes for new queries
- Unbuffered writes in hot paths (especially ClickHouse)
- Missing LIMIT clauses on potentially large result sets

### Bugs
- Nil pointer risks (especially on struct pointer params and optional relations)
- Functions returning `nil, nil` (violates convention)
- Missing error handling
- Race conditions in concurrent code paths

### Security
- Hardcoded secrets or sensitive data exposure
- Missing input validation on service request structs

### Tests
- Are there tests for the new/changed code?
- Do the tests cover edge cases and error paths?
- Are test assertions specific (not just "no error")?

## Step 5: Present the Review

Structure your review as:

```
## Summary
[1-2 sentences: what this PR does and overall assessment]

## Requirements Check
[Does the PR fulfill the Linear ticket / PR description requirements? Any gaps?]

## Issues
### Critical (must fix before merge)
- [blocking issues]

### Suggestions (nice to have)
- [non-blocking improvements]

## Prior Review Activity
[Summarize what other reviewers have flagged, attributed by name. Note which of their concerns have been addressed in the current code and which remain open.]

## Verdict
[LGTM / Needs changes / Needs discussion -- based on YOUR analysis, not other reviewers' findings]
```

## Guidelines

- Be concise. Don't pad with praise or filler.
- Only raise issues that matter. Don't nitpick formatting (that's what linters are for).
- Quote code snippets rather than referencing line numbers.
- If PR comments show a discussion was already resolved, don't reopen it.
- If you're unsure about something, flag it as a question rather than a definitive issue.
2 changes: 1 addition & 1 deletion .fernignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Specify files that shouldn't be modified by Fern
.claude/settings.json
.claude/
.github/CODEOWNERS
.github/workflows/claude-code.yml
CLAUDE.md
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -482,3 +482,6 @@ $RECYCLE.BIN/

# Vim temporary swap files
*.swp

# Claude Code local settings
.claude/settings.local.json
2 changes: 1 addition & 1 deletion examples/DatastreamTestServer/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@
var redisConfig = new RedisCacheConfig
{
Endpoints = new List<string> { redisUrl },
KeyPrefix = redisKeyPrefix
KeyPrefix = ""
};

var datastreamOptions = new DatastreamOptions
Expand Down
32 changes: 26 additions & 6 deletions src/SchematicHQ.Client.Test/Datastream/CompanyMetricsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
using SchematicHQ.Client.Test.Datastream.Mocks;
using SchematicHQ.Client.Core;
using SchematicHQ.Client.Datastream;
using SchematicHQ.Client.RulesEngine.Models;

namespace SchematicHQ.Client.Test.Datastream
{
Expand All @@ -30,7 +29,7 @@ public class CompanyMetricsTests : IDisposable

// Client and dependencies
private DatastreamClient _client;
private ICacheProvider<Company> _companyCache;
private ICacheProvider<RulesengineCompany> _companyCache;
private ICacheProvider<string> _companyLookupCache;

[SetUp]
Expand All @@ -41,7 +40,7 @@ public void Setup()
_client = client;
// Use reflection to get the private cache fields
var cacheField = typeof(DatastreamClient).GetField("_companyCache", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
_companyCache = (ICacheProvider<Company>?)cacheField?.GetValue(_client) ?? throw new Exception("Could not get company cache");
_companyCache = (ICacheProvider<RulesengineCompany>?)cacheField?.GetValue(_client) ?? throw new Exception("Could not get company cache");

var lookupCacheField = typeof(DatastreamClient).GetField("_companyLookupCache", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
_companyLookupCache = (ICacheProvider<string>?)lookupCacheField?.GetValue(_client) ?? throw new Exception("Could not get company lookup cache");
Expand All @@ -64,7 +63,7 @@ private string ResourceKeyToCacheKey(string keyName, string keyValue)
{
var method = typeof(DatastreamClient).GetMethod("ResourceKeyToCacheKey",
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
var genericMethod = method!.MakeGenericMethod(typeof(Company));
var genericMethod = method!.MakeGenericMethod(typeof(RulesengineCompany));
return (string)genericMethod.Invoke(_client, new object[] { CacheKeyPrefixCompany, keyName, keyValue })!;
}

Expand All @@ -83,7 +82,7 @@ private string CompanyIdCacheKey(string id)
/// </summary>
private void SetupCompanyInCache(string companyJson)
{
var company = JsonSerializer.Deserialize<Company>(companyJson, new JsonSerializerOptions
var company = JsonSerializer.Deserialize<RulesengineCompany>(companyJson, new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
});
Expand All @@ -108,7 +107,7 @@ private void SetupCompanyInCache(string companyJson)
/// <summary>
/// Helper method to get a company from cache using two-step lookup
/// </summary>
private Company? GetCompanyFromCache(Dictionary<string, string> keys)
private RulesengineCompany? GetCompanyFromCache(Dictionary<string, string> keys)
{
if (keys.Count == 0) return null;

Expand Down Expand Up @@ -181,6 +180,9 @@ public void UpdateCompanyMetrics_WithNoMatchingMetric_ReturnsFalse()
],
""metrics"": [
{
""account_id"": ""acc123"",
""company_id"": ""company123"",
""environment_id"": ""env123"",
""event_subtype"": ""metric1"",
""period"": ""all_time"",
""month_reset"": ""first_of_month"",
Expand Down Expand Up @@ -223,6 +225,9 @@ public void UpdateCompanyMetrics_WithMatchingMetric_UpdatesValueWithDefaultQuant
],
""metrics"": [
{
""account_id"": ""acc123"",
""company_id"": ""company123"",
""environment_id"": ""env123"",
""event_subtype"": ""metric1"",
""period"": ""all_time"",
""month_reset"": ""first_of_month"",
Expand Down Expand Up @@ -271,6 +276,9 @@ public void UpdateCompanyMetrics_WithSpecificQuantity_UpdatesMetricValue()
],
""metrics"": [
{
""account_id"": ""acc123"",
""company_id"": ""company123"",
""environment_id"": ""env123"",
""event_subtype"": ""metric1"",
""period"": ""all_time"",
""month_reset"": ""first_of_month"",
Expand Down Expand Up @@ -320,6 +328,9 @@ public void UpdateCompanyMetrics_WithMultipleKeys_UpdatesAllCacheEntries()
],
""metrics"": [
{
""account_id"": ""acc123"",
""company_id"": ""company123"",
""environment_id"": ""env123"",
""event_subtype"": ""metric1"",
""period"": ""all_time"",
""month_reset"": ""first_of_month"",
Expand Down Expand Up @@ -378,20 +389,29 @@ public void UpdateCompanyMetrics_WithMultipleMetrics_UpdatesAllMatchingMetrics()
],
""metrics"": [
{
""account_id"": ""acc123"",
""company_id"": ""company123"",
""environment_id"": ""env123"",
""event_subtype"": ""metric1"",
""period"": ""all_time"",
""month_reset"": ""first_of_month"",
""value"": 100,
""created_at"": ""2023-01-01T00:00:00Z""
},
{
""account_id"": ""acc123"",
""company_id"": ""company123"",
""environment_id"": ""env123"",
""event_subtype"": ""metric1"",
""period"": ""current_month"",
""month_reset"": ""first_of_month"",
""value"": 50,
""created_at"": ""2023-01-01T00:00:00Z""
},
{
""account_id"": ""acc123"",
""company_id"": ""company123"",
""environment_id"": ""env123"",
""event_subtype"": ""metric2"",
""period"": ""all_time"",
""month_reset"": ""first_of_month"",
Expand Down
13 changes: 6 additions & 7 deletions src/SchematicHQ.Client.Test/Datastream/DatastreamCacheTests.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
using System.Text.Json;
using NUnit.Framework;
using SchematicHQ.Client.RulesEngine.Models;
using SchematicHQ.Client.Datastream;
using SchematicHQ.Client.Test.Datastream.Mocks;

Expand All @@ -22,14 +21,14 @@ public void Setup()
_mockLogger = testSetup.Logger;
}

private void PopulateTwoLayerCompanyCache(Company company)
private void PopulateTwoLayerCompanyCache(RulesengineCompany company)
{
var method = typeof(DatastreamClient).GetMethod("CacheCompanyForKeys",
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
method!.Invoke(_client, new object[] { company });
}

private void PopulateTwoLayerUserCache(User user)
private void PopulateTwoLayerUserCache(RulesengineUser user)
{
var method = typeof(DatastreamClient).GetMethod("CacheUserForKeys",
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
Expand All @@ -41,7 +40,7 @@ public void ExpiredCache_RequestsResourcesAgain()
{
var companyKey = "company-123";

var company = new Company
var company = new RulesengineCompany
{
AccountId = "acc_123",
EnvironmentId = "env_123",
Expand All @@ -66,7 +65,7 @@ public void ExpiredCache_RequestsResourcesAgain()
[Test]
public void TwoStepCompanyLookup_CacheAndRetrieve()
{
var company = new Company
var company = new RulesengineCompany
{
AccountId = "acc_123",
EnvironmentId = "env_123",
Expand All @@ -87,7 +86,7 @@ public void TwoStepCompanyLookup_CacheAndRetrieve()
[Test]
public void TwoStepUserLookup_CacheAndRetrieve()
{
var user = new User
var user = new RulesengineUser
{
AccountId = "acc_123",
EnvironmentId = "env_123",
Expand All @@ -108,7 +107,7 @@ public void TwoStepUserLookup_CacheAndRetrieve()
[Test]
public void MultipleResourceKeys_ResolveToSameObject()
{
var company = new Company
var company = new RulesengineCompany
{
AccountId = "acc_123",
EnvironmentId = "env_123",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,7 @@ public async Task CheckFlag_WhenNotConnected_ThrowsException()
var flagsCache = flagsCacheField!.GetValue(client);

// Create a test flag
var flag = new SchematicHQ.Client.RulesEngine.Models.Flag
var flag = new RulesengineFlag
{
Id = "flag_123",
Key = "test-flag",
Expand Down
Loading
Loading