Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Conversation

@hiteshshimpi-55
Copy link
Contributor

@hiteshshimpi-55 hiteshshimpi-55 commented Nov 7, 2025

πŸ“ Description

  1. Test case failing issue (switch case had else at the bottom)
  2. Refactored Feature usage reprocessing script

πŸ”¨ Changes Made

  • Feature 1
  • Bug Fix
  • Refactor
  • Documentation Update

βœ… Checklist

  • My code follows the project's code style.
  • I have added tests where applicable.
  • All new and existing tests pass.
  • I have updated documentation (if needed).

Important

Fix test case in billing.go, add FindUnprocessedEventsFromFeatureUsage() in event.go, and refactor PublishEvent() in feature_usage_tracking.go.

  • Bug Fix:
    • Fix switch case in AggregateEntitlements() in billing.go to use case instead of else if for types.ENTITLEMENT_ENTITY_TYPE_SUBSCRIPTION.
  • Feature:
    • Add FindUnprocessedEventsFromFeatureUsage() to event.go for querying unprocessed events using ClickHouse.
  • Refactor:
    • Refactor PublishEvent() in feature_usage_tracking.go to streamline backfill logic.
  • Test:
    • Add stub for FindUnprocessedEventsFromFeatureUsage() in inmemory_event_store.go.

This description was created by Ellipsis for b2b5a01. You can customize this summary. It will automatically update as commits are pushed.

Summary by CodeRabbit

  • New Features

    • Added capability to retrieve unprocessed events from feature usage tracking contexts.
  • Bug Fixes

    • Fixed event publishing in backfill mode to use correct subscription and topic assignments.

@coderabbitai
Copy link

coderabbitai bot commented Nov 7, 2025

Walkthrough

A new repository method FindUnprocessedEventsFromFeatureUsage is introduced across the domain, repository layer, test utilities, and service layer to retrieve unprocessed events via anti-join on a feature usage context. Related refactoring includes variable scoping adjustments for backfill publishing and control flow consolidation for subscription entity handling.

Changes

Cohort / File(s) Change Summary
Interface definition
internal/domain/events/repository.go
Added FindUnprocessedEventsFromFeatureUsage method signature to Repository interface for retrieving unprocessed events from feature usage context.
Repository implementation
internal/repository/clickhouse/event.go
Implemented FindUnprocessedEventsFromFeatureUsage with ANTI JOIN query logic, keyset pagination (timestamp, id), and support for filters (ExternalCustomerID, EventName, StartTime, EndTime). Note: Method defined twice with identical implementation.
Service layer refactoring
internal/service/feature_usage_tracking.go
Refactored PublishEvent variable scoping for backfill pubSub/topic handling; replaced calls to FindUnprocessedEvents with FindUnprocessedEventsFromFeatureUsage in ReprocessEvents.
Service layer control flow
internal/service/billing.go
Converted else-if branch for SUBSCRIPTION entity type into dedicated switch case; functional outcome unchanged.
Test utility stub
internal/testutil/inmemory_event_store.go
Added stub method FindUnprocessedEventsFromFeatureUsage returning not-implemented error.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

  • Duplicate method definition: The FindUnprocessedEventsFromFeatureUsage method is defined twice identically in internal/repository/clickhouse/event.goβ€”verify this is unintentional and should be removed.
  • Query logic verification: Review the ANTI JOIN construction and keyset pagination logic in the new repository method for correctness and performance.
  • Call-site migration: Confirm that replacing FindUnprocessedEvents with FindUnprocessedEventsFromFeatureUsage in feature_usage_tracking.go is intentional and semantically correct.
  • Variable scoping: Ensure the backfill publishing refactoring in PublishEvent preserves intended behavior and doesn't affect non-backfill code paths.

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Title check ⚠️ Warning The PR title 'Fix/test cases billing' does not follow the required naming convention format: type(module): message. Rename the PR title to follow the convention, such as 'fix(billing): resolve failing test case from switch statement' or 'fix(billing): refactor feature usage reprocessing logic'.
βœ… Passed checks (2 passed)
Check name Status Explanation
Description Check βœ… Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage βœ… Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing touches
  • πŸ“ Generate docstrings
πŸ§ͺ Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❀️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@ellipsis-dev ellipsis-dev bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Important

Looks good to me! πŸ‘

Reviewed everything up to b2b5a01 in 38 seconds. Click for details.
  • Reviewed 246 lines of code in 5 files
  • Skipped 0 files when reviewing.
  • Skipped posting 1 draft comments. View those below.
  • Modify your settings and rules to customize what types of comments Ellipsis leaves. And don't forget to react with πŸ‘ or πŸ‘Ž to teach Ellipsis.
1. internal/testutil/inmemory_event_store.go:678
  • Draft comment:
    The methods FindUnprocessedEvents and FindUnprocessedEventsFromFeatureUsage are stubbed to return a 'not implemented' error. Be sure to add a comment/TODO explaining when a lightweight in‑memory simulation might be needed for tests that exercise these methods.
  • Reason this comment was not posted:
    Comment was not on a location in the diff, so it can't be submitted as a review comment.

Workflow ID: wflow_1dEb9VZRDfYncqDJ

You can customize Ellipsis by changing your verbosity settings, reacting with πŸ‘ or πŸ‘Ž, replying to comments, or adding code review rules.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
internal/service/billing.go (1)

1345-1349: LGTM! Clean refactoring from else-if to switch case.

Converting the subscription entity type handling to a proper case statement is more idiomatic and improves code clarity. This aligns with the PR objective of fixing the test case issue.

Consider adding a default case to the switch for defensive programming:

 case types.ENTITLEMENT_ENTITY_TYPE_SUBSCRIPTION:
     entityType = dto.EntitlementSourceEntityTypeSubscription
     // For subscription entitlements, entity_name can be left empty or set to subscription identifier
     // The entity_id is the subscription ID itself
+default:
+    // Log unexpected entity type or handle as needed
+    s.Logger.Warnw("unexpected entitlement entity type, defaulting to plan",
+        "entity_type", ent.EntityType,
+        "entitlement_id", ent.ID)
 }
πŸ“œ Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

πŸ“₯ Commits

Reviewing files that changed from the base of the PR and between e9b70c8 and b2b5a01.

πŸ“’ Files selected for processing (5)
  • internal/domain/events/repository.go (1 hunks)
  • internal/repository/clickhouse/event.go (1 hunks)
  • internal/service/billing.go (1 hunks)
  • internal/service/feature_usage_tracking.go (2 hunks)
  • internal/testutil/inmemory_event_store.go (1 hunks)
🧰 Additional context used
🧠 Learnings (2)
πŸ“š Learning: 2025-10-18T10:22:48.348Z
Learnt from: omkar273
Repo: flexprice/flexprice PR: 652
File: internal/service/billing.go:1454-1477
Timestamp: 2025-10-18T10:22:48.348Z
Learning: In `internal/service/billing.go`, the `GetCustomerEntitlements` function passes `subscriptions[0].ID` to `AggregateEntitlements` as a fallback subscription ID. This is acceptable because customers typically have a single subscription, and the subscription ID parameter is only used as fallback metadata in `EntitlementSource` objects, not for business logic or filtering.

Applied to files:

  • internal/service/billing.go
πŸ“š Learning: 2025-10-17T21:53:42.505Z
Learnt from: omkar273
Repo: flexprice/flexprice PR: 652
File: internal/service/subscription.go:3495-3534
Timestamp: 2025-10-17T21:53:42.505Z
Learning: In `internal/service/subscription.go`, when adding multiple-type addons to a subscription, existing active line items are intentionally reused rather than creating new line items for each addon association. The addon occurrences are tracked via separate `AddonAssociation` records, but billing happens through the shared `SubscriptionLineItem`. This is the expected behavior and should not be flagged as an issue.

Applied to files:

  • internal/service/billing.go
🧬 Code graph analysis (5)
internal/testutil/inmemory_event_store.go (2)
internal/domain/events/model.go (2)
  • FindUnprocessedEventsParams (79-87)
  • Event (13-45)
internal/errors/errors.go (1)
  • ErrSystem (21-21)
internal/domain/events/repository.go (1)
internal/domain/events/model.go (2)
  • FindUnprocessedEventsParams (79-87)
  • Event (13-45)
internal/service/billing.go (1)
internal/types/entitlement.go (1)
  • ENTITLEMENT_ENTITY_TYPE_SUBSCRIPTION (49-49)
internal/repository/clickhouse/event.go (5)
internal/domain/events/model.go (2)
  • FindUnprocessedEventsParams (79-87)
  • Event (13-45)
internal/repository/clickhouse/helper.go (4)
  • StartRepositorySpan (11-35)
  • FinishSpan (38-42)
  • SetSpanError (45-52)
  • SetSpanSuccess (55-59)
internal/types/context.go (2)
  • GetTenantID (33-38)
  • GetEnvironmentID (54-59)
internal/errors/builder.go (1)
  • WithError (26-28)
internal/errors/errors.go (2)
  • ErrDatabase (20-20)
  • ErrValidation (16-16)
internal/service/feature_usage_tracking.go (2)
internal/errors/errors.go (1)
  • ErrSystem (21-21)
internal/errors/builder.go (1)
  • WithError (26-28)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Run Tests and Lint
πŸ”‡ Additional comments (4)
internal/domain/events/repository.go (1)

18-18: LGTM! Clean interface addition.

The new method FindUnprocessedEventsFromFeatureUsage follows the existing pattern and clearly indicates its purpose through naming.

internal/testutil/inmemory_event_store.go (1)

684-688: LGTM! Proper test stub implementation.

The stub correctly satisfies the interface while clearly indicating the method is not implemented for the in-memory store.

internal/service/feature_usage_tracking.go (2)

144-169: LGTM! Improved variable scoping eliminates shadowing.

The refactored approach declares pubSub and topic once at the outer scope and conditionally assigns them based on isBackfill, eliminating potential variable shadowing issues. The nil check at line 151 ensures safe usage.


1907-1907: LGTM! Correct method selection for feature usage reprocessing.

The switch to FindUnprocessedEventsFromFeatureUsage aligns with the refactoring objective to use a feature-usage-specific unprocessed events query.

Comment on lines +820 to +950
// FindUnprocessedEvents finds events that haven't been processed yet
// Uses keyset pagination for better performance with large datasets
func (r *EventRepository) FindUnprocessedEventsFromFeatureUsage(ctx context.Context, params *events.FindUnprocessedEventsParams) ([]*events.Event, error) {
span := StartRepositorySpan(ctx, "event", "find_unprocessed_events", map[string]interface{}{
"batch_size": params.BatchSize,
"external_customer_id": params.ExternalCustomerID,
})
defer FinishSpan(span)

// Use ANTI JOIN for better performance with ClickHouse
// This avoids the need for subqueries in the WHERE clause
// Also using the primary key ORDER BY for efficiency
query := `
SELECT
e.id, e.external_customer_id, e.customer_id, e.tenant_id,
e.event_name, e.timestamp, e.source, e.properties,
e.environment_id, e.ingested_at
FROM events e
ANTI JOIN (
SELECT id, tenant_id, environment_id
FROM feature_usage
WHERE tenant_id = ?
AND environment_id = ?
) AS p
ON e.id = p.id AND e.tenant_id = p.tenant_id AND e.environment_id = p.environment_id
WHERE e.tenant_id = ?
AND e.environment_id = ?
`

args := []interface{}{
types.GetTenantID(ctx),
types.GetEnvironmentID(ctx),
types.GetTenantID(ctx),
types.GetEnvironmentID(ctx),
}

// Add the last seen ID and timestamp for keyset pagination if provided
if params.LastID != "" && !params.LastTimestamp.IsZero() {
// Use keyset pagination for better performance
query += " AND (e.timestamp, e.id) < (?, ?)"
args = append(args, params.LastTimestamp, params.LastID)
}

// Add filters if provided
if params.ExternalCustomerID != "" {
query += " AND e.external_customer_id = ?"
args = append(args, params.ExternalCustomerID)
}

if params.EventName != "" {
query += " AND e.event_name = ?"
args = append(args, params.EventName)
}

if !params.StartTime.IsZero() {
query += " AND e.timestamp >= ?"
args = append(args, params.StartTime)
}

if !params.EndTime.IsZero() {
query += " AND e.timestamp <= ?"
args = append(args, params.EndTime)
}

// Add sorting for consistent keyset pagination
// Using the same fields we're filtering on for the keyset
query += " ORDER BY e.timestamp DESC, e.id DESC"

// Add batch size limit
if params.BatchSize > 0 {
query += " LIMIT ?"
args = append(args, params.BatchSize)
} else {
// Default to a reasonable batch size to avoid huge result sets
query += " LIMIT 100"
}

r.logger.Debugw("executing find unprocessed events query",
"query", query,
"external_customer_id", params.ExternalCustomerID,
"event_name", params.EventName,
"batch_size", params.BatchSize,
)

// Execute the query
rows, err := r.store.GetConn().Query(ctx, query, args...)
if err != nil {
SetSpanError(span, err)
return nil, ierr.WithError(err).
WithHint("Failed to query unprocessed events").
Mark(ierr.ErrDatabase)
}
defer rows.Close()

var eventsList []*events.Event
for rows.Next() {
var event events.Event
var propertiesJSON string

err := rows.Scan(
&event.ID,
&event.ExternalCustomerID,
&event.CustomerID,
&event.TenantID,
&event.EventName,
&event.Timestamp,
&event.Source,
&propertiesJSON,
&event.EnvironmentID,
&event.IngestedAt,
)
if err != nil {
SetSpanError(span, err)
return nil, ierr.WithError(err).
WithHint("Failed to scan event").
Mark(ierr.ErrDatabase)
}

if err := json.Unmarshal([]byte(propertiesJSON), &event.Properties); err != nil {
SetSpanError(span, err)
return nil, ierr.WithError(err).
WithHint("Failed to unmarshal event properties").
Mark(ierr.ErrValidation)
}

eventsList = append(eventsList, &event)
}

SetSpanSuccess(span)
return eventsList, nil
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

πŸ› οΈ Refactor suggestion | 🟠 Major

Extract common logic to eliminate ~130 lines of duplication.

The FindUnprocessedEventsFromFeatureUsage method is nearly identical to FindUnprocessedEvents (lines 688-818), with the only difference being the table name in the ANTI JOIN query (feature_usage vs events_processed). This creates significant maintenance burden as bug fixes must be applied to both methods.

Consider refactoring to a shared helper function:

// findUnprocessedEventsFromTable finds events that haven't been processed in the specified table
func (r *EventRepository) findUnprocessedEventsFromTable(
	ctx context.Context, 
	params *events.FindUnprocessedEventsParams,
	tableName string,
	spanOperation string,
) ([]*events.Event, error) {
	span := StartRepositorySpan(ctx, "event", spanOperation, map[string]interface{}{
		"batch_size":           params.BatchSize,
		"external_customer_id": params.ExternalCustomerID,
		"table":                tableName,
	})
	defer FinishSpan(span)

	query := fmt.Sprintf(`
		SELECT 
			e.id, e.external_customer_id, e.customer_id, e.tenant_id, 
			e.event_name, e.timestamp, e.source, e.properties, 
			e.environment_id, e.ingested_at
		FROM events e
		ANTI JOIN (
			SELECT id, tenant_id, environment_id
			FROM %s
			WHERE tenant_id = ?
			AND environment_id = ?
		) AS p
		ON e.id = p.id AND e.tenant_id = p.tenant_id AND e.environment_id = p.environment_id
		WHERE e.tenant_id = ?
		AND e.environment_id = ?
	`, tableName)
	
	// ... rest of the common logic
}

func (r *EventRepository) FindUnprocessedEvents(ctx context.Context, params *events.FindUnprocessedEventsParams) ([]*events.Event, error) {
	return r.findUnprocessedEventsFromTable(ctx, params, "events_processed", "find_unprocessed_events")
}

func (r *EventRepository) FindUnprocessedEventsFromFeatureUsage(ctx context.Context, params *events.FindUnprocessedEventsParams) ([]*events.Event, error) {
	return r.findUnprocessedEventsFromTable(ctx, params, "feature_usage", "find_unprocessed_events_from_feature_usage")
}
πŸ€– Prompt for AI Agents
internal/repository/clickhouse/event.go lines 820-950: the method duplicates
~130 lines from FindUnprocessedEvents; extract the shared query construction,
arg assembly, query execution and row scanning into a helper (e.g.
findUnprocessedEventsFromTable) that accepts ctx, params, tableName and
spanOperation; build the ANTI JOIN query using fmt.Sprintf to inject tableName,
preserve all existing param handling (LastID/LastTimestamp, ExternalCustomerID,
EventName, StartTime, EndTime, ORDER BY, LIMIT) and logging/span behavior
(include table in span tags), then update FindUnprocessedEvents and
FindUnprocessedEventsFromFeatureUsage to simply call the helper with
"events_processed" and "feature_usage" respectively and return its result.

// FindUnprocessedEvents finds events that haven't been processed yet
// Uses keyset pagination for better performance with large datasets
func (r *EventRepository) FindUnprocessedEventsFromFeatureUsage(ctx context.Context, params *events.FindUnprocessedEventsParams) ([]*events.Event, error) {
span := StartRepositorySpan(ctx, "event", "find_unprocessed_events", map[string]interface{}{
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟑 Minor

Update span operation name to distinguish from the other method.

The span operation is named "find_unprocessed_events", identical to the existing FindUnprocessedEvents method at line 691. This makes it difficult to distinguish between the two operations in traces and monitoring.

Apply this diff:

-	span := StartRepositorySpan(ctx, "event", "find_unprocessed_events", map[string]interface{}{
+	span := StartRepositorySpan(ctx, "event", "find_unprocessed_events_from_feature_usage", map[string]interface{}{
πŸ“ Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
span := StartRepositorySpan(ctx, "event", "find_unprocessed_events", map[string]interface{}{
span := StartRepositorySpan(ctx, "event", "find_unprocessed_events_from_feature_usage", map[string]interface{}{
πŸ€– Prompt for AI Agents
In internal/repository/clickhouse/event.go around line 823, the
StartRepositorySpan call uses the operation name "find_unprocessed_events",
which duplicates the span name used by the other FindUnprocessedEvents method at
line 691; change the operation name to a distinct, descriptive string (for
example include method differentiator like "find_unprocessed_events_stream" or
"find_unprocessed_events_with_cursor" or mirror the current function name) so
traces/monitoring can distinguish the two operations, keeping the rest of the
span attributes unchanged.

@hiteshshimpi-55 hiteshshimpi-55 merged commit 9f1ad12 into flexprice:develop Nov 7, 2025
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant