Skip to content

feat: persistent retrieval stats with batched flush#27

Merged
thebtf merged 1 commit into
mainfrom
feat/retrieval-stats-logger
Mar 20, 2026
Merged

feat: persistent retrieval stats with batched flush#27
thebtf merged 1 commit into
mainfrom
feat/retrieval-stats-logger

Conversation

@thebtf
Copy link
Copy Markdown
Owner

@thebtf thebtf commented Mar 20, 2026

Summary

  • Implements Phase 3 (T3.2-T3.5) from Dashboard Analytics v2 spec
  • Creates RetrievalStatsLogStore with bounded channel + background batched flusher
  • Wires retrieval event logging into existing recordRetrievalStatsExtended
  • Updates /api/stats/retrieval to aggregate from DB with since parameter
  • Adds 90-day cleanup for retrieval_stats_log table
  • Migration 038 (table creation) was already merged in PR feat: Dashboard Analytics v2 — persistent analytics, bugfixes, verified facts TTL #26

Details

  • Bounded channel (1000 cap) with non-blocking drop on overflow
  • Background goroutine flushes every 5s or at 50 entries
  • Graceful shutdown via Close() drains remaining entries
  • Falls back to in-memory stats if DB store unavailable
  • Frontend RetrievalStatsResponse type extended with new fields

Test plan

  • Verify build passes (Go + TypeScript)
  • Deploy and trigger searches to populate retrieval_stats_log
  • Query /api/stats/retrieval?since=<ISO8601> and verify aggregated data
  • Trigger maintenance and verify 90-day cleanup runs
  • Verify dashboard Analytics page shows retrieval stats with time range

Summary by CodeRabbit

Релиз

  • New Features
    • Добавлена история статистики получения данных с поддержкой фильтрации по времени.
    • Введены новые метрики: исключённые устаревшие данные, количество свежих данных и удалённые дубликаты.
    • Реализована автоматическая очистка истории статистики (90-дневное хранилище).

- Create RetrievalStatsLogStore with bounded channel + background flusher
- Batch inserts every 5s or 50 entries, non-blocking drop on full channel
- Wire into recordRetrievalStatsExtended (search_request, context_injection,
  observations_served, stale_excluded, fresh_count, duplicates_removed)
- Update handleGetRetrievalStats to aggregate from DB with since param,
  fall back to in-memory stats
- Add 90-day cleanup for retrieval_stats_log in maintenance handler
- Add retrievalStatsLogStore to reinitializeDatabase path
- Update frontend RetrievalStatsResponse type with new fields
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 20, 2026

Walkthrough

Добавлена постоянная база данных для сбора статистики извлечения (retrieval statistics) с фоновым сохранением событий в пакетах. Реализована логика очистки старых логов, интеграция с обработчиками запросов и обновлен API интерфейс для поддержки временных фильтров.

Changes

Cohort / File(s) Summary
Новое хранилище статистики извлечения
internal/db/gorm/retrieval_stats_log_store.go
Реализовано GORM-хранилище с типом RetrievalStatsLogEntry, фоновым сборщиком событий с периодической flush операцией в батчах, методами для логирования событий, получения агрегированной статистики с поддержкой временных фильтров и очистки старых записей.
Интеграция в обработчики данных
internal/worker/handlers_data.go
Добавлен необязательный параметр запроса since в RFC3339 формате для фильтрации статистики по времени. При наличии хранилища возвращаются статистика из БД, в противном случае используется существующий in-memory путь.
Инициализация и проводка
internal/worker/service.go
Добавлено поле retrievalStatsLogStore в структуру Service, инициализируемое при загрузке с передачей событий из recordRetrievalStatsExtended, пересоздаваемое при переинициализации БД и безопасно закрываемое через мьютекс доступа.
Фоновая очистка логов
internal/worker/handlers_maintenance.go
Добавлена горутина для удаления логов статистики старше 90 дней, выполняемая во время обслуживания и безопасно заключенная в мьютекс.
Обновление интерфейса API
ui/src/utils/api.ts
Расширен интерфейс RetrievalStatsResponse добавлением трех новых полей: stale_excluded, fresh_count, duplicates_removed.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant Handler
    participant Service
    participant Store as RetrievalStatsLogStore
    participant DB as Database

    Client->>Handler: GET /retrieval-stats?since=<time>
    activate Handler
    Handler->>Service: initMu.RLock()
    Handler->>Service: retrievalStatsLogStore
    Service-->>Handler: logStore reference
    Handler->>Service: initMu.RUnlock()
    
    alt Store Available
        Handler->>Store: GetStats(ctx, project, since)
        activate Store
        Store->>DB: Query retrieval_stats_log<br/>GROUP BY event_type
        DB-->>Store: Rows with sums
        Store->>Store: Aggregate into<br/>AggregatedRetrievalStats
        Store-->>Handler: Stats
        deactivate Store
    else Store Unavailable
        Handler->>Service: GetRetrievalStats(project)<br/>(in-memory, no time filter)
        Service-->>Handler: Stats
    end
    
    Handler-->>Client: JSON Response
    deactivate Handler

    rect rgba(100, 150, 200, 0.5)
    Note over Service,DB: Background: Event Logging & Flushing
    Service->>Store: LogEvent(project, eventType, count)<br/>(non-blocking channel)
    Store->>Store: Buffer event in queue
    Store->>Store: Flusher goroutine<br/>periodically flushes
    Store->>DB: CreateInBatches(entries)
    DB-->>Store: OK
    end

    rect rgba(100, 200, 100, 0.5)
    Note over Service,DB: Background: Cleanup (90-day retention)
    Service->>Store: Cleanup(ctx, 90*24*hour)
    Store->>DB: DELETE FROM retrieval_stats_log<br/>WHERE created_at < cutoff
    DB-->>Store: rows affected count
    Store-->>Service: count, error
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 Хранилище новое встало с утра,
Событья спешат, но спешить им пора—
В батчах сбегаются, в БД улетают,
Логи старье ровно в сорок пять дней стирают,
Статистик танец — прекрасный и звонкий! 📊✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: persistent retrieval stats with batched flush' accurately and specifically describes the main change: implementing a persistent (database-backed) retrieval statistics store with batched flushing to the database.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/retrieval-stats-logger
📝 Coding Plan
  • Generate coding plan for human review comments

Warning

Review ran into problems

🔥 Problems

Git: Failed to clone repository. Please run the @coderabbitai full review command to re-trigger a full review. If the issue persists, set path_filters to include or exclude specific files.


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

@gemini-code-assist
Copy link
Copy Markdown

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly enhances the analytics capabilities by introducing persistent storage for retrieval statistics. It transitions from an ephemeral in-memory system to a robust database-backed solution, featuring efficient batched writes and automated data cleanup. This change provides more reliable and comprehensive historical data for dashboard analytics, allowing for time-based querying and improved data retention.

Highlights

  • Persistent Retrieval Stats: Implemented a new RetrievalStatsLogStore to persistently log retrieval events to a PostgreSQL database, replacing previous in-memory storage.
  • Batched Flushing: Introduced a background goroutine that batches retrieval events and flushes them to the database periodically (every 5 seconds) or when a threshold (50 entries) is reached, using a bounded channel (1000 cap) for efficient, non-blocking logging.
  • API Enhancement: Updated the /api/stats/retrieval endpoint to query aggregated retrieval statistics directly from the database, now supporting a 'since' parameter for time-range filtering.
  • Data Retention Policy: Added a maintenance task to automatically clean up retrieval_stats_log entries older than 90 days, ensuring data hygiene.
  • Graceful Shutdown & Fallback: Implemented graceful shutdown for the log store to drain remaining entries and included a fallback mechanism to in-memory stats if the database store is unavailable.
  • Frontend Type Update: Extended the RetrievalStatsResponse type in the frontend to include new detailed statistics fields.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@thebtf
Copy link
Copy Markdown
Owner Author

thebtf commented Mar 20, 2026

@coderabbitai review

@thebtf
Copy link
Copy Markdown
Owner Author

thebtf commented Mar 20, 2026

@gemini-code-assist review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 20, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@thebtf
Copy link
Copy Markdown
Owner Author

thebtf commented Mar 20, 2026

@codex review

Copy link
Copy Markdown

@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.

🧹 Nitpick comments (2)
internal/worker/handlers_data.go (1)

438-443: Рассмотрите возврат ошибки для невалидного параметра since.

При невалидном формате даты (не RFC3339) ошибка парсинга игнорируется, и запрос возвращает статистику за всё время. Это может ввести пользователя в заблуждение.

Для консистентности с handleGetSearchAnalytics (строки 524-530), где аналогичная ошибка возвращает HTTP 400, рекомендуется добавить валидацию:

♻️ Предлагаемое исправление
 	if logStore != nil {
 		var since time.Time
 		if sinceStr := r.URL.Query().Get("since"); sinceStr != "" {
-			if t, err := time.Parse(time.RFC3339, sinceStr); err == nil {
-				since = t
+			t, err := time.Parse(time.RFC3339, sinceStr)
+			if err != nil {
+				http.Error(w, "invalid 'since' parameter: "+err.Error(), http.StatusBadRequest)
+				return
 			}
+			since = t
 		}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/worker/handlers_data.go` around lines 438 - 443, The handler
currently swallows time.Parse errors for the "since" query (code using sinceStr
:= r.URL.Query().Get("since") and time.Parse(time.RFC3339, sinceStr)), causing
invalid dates to be treated as "no since" and returning all-time stats; change
this to validate the parse and return an HTTP 400 with a clear message when
parsing fails (mirror the behavior in handleGetSearchAnalytics which checks the
parse and responds with a 400), i.e., if time.Parse returns an error for
sinceStr, call the same error response path instead of ignoring the error.
internal/db/gorm/retrieval_stats_log_store.go (1)

101-106: Рассмотрите добавление таймаута для операций flush при завершении работы.

Метод flush выполняет запись в БД без контекста с таймаутом. При медленной базе данных или сетевых проблемах операция Close() может зависнуть на неопределённое время, блокируя graceful shutdown.

♻️ Предлагаемое исправление
-func (s *RetrievalStatsLogStore) flush(entries []RetrievalStatsLogEntry) {
-	if err := s.db.CreateInBatches(entries, len(entries)).Error; err != nil {
+func (s *RetrievalStatsLogStore) flush(entries []RetrievalStatsLogEntry) {
+	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+	defer cancel()
+	if err := s.db.WithContext(ctx).CreateInBatches(entries, len(entries)).Error; err != nil {
 		log.Warn().Err(err).Int("batch_size", len(entries)).Msg("failed to flush retrieval stats batch")
 	}
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/db/gorm/retrieval_stats_log_store.go` around lines 101 - 106, The
flush method does DB writes without a timeout, risking Close() hanging; modify
RetrievalStatsLogStore.flush to create a context with a timeout (e.g.,
context.WithTimeout(ctx, 5*time.Second)) and call
s.db.WithContext(ctx).CreateInBatches(entries, len(entries)) so the DB operation
is cancellable; also update whichever Close() or shutdown path calls flush to
either pass through a parent context or use the same bounded timeout and ensure
the cancel() is deferred to free resources; make the timeout a configurable
constant or field (e.g., flushTimeout) on RetrievalStatsLogStore for
testability.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@internal/db/gorm/retrieval_stats_log_store.go`:
- Around line 101-106: The flush method does DB writes without a timeout,
risking Close() hanging; modify RetrievalStatsLogStore.flush to create a context
with a timeout (e.g., context.WithTimeout(ctx, 5*time.Second)) and call
s.db.WithContext(ctx).CreateInBatches(entries, len(entries)) so the DB operation
is cancellable; also update whichever Close() or shutdown path calls flush to
either pass through a parent context or use the same bounded timeout and ensure
the cancel() is deferred to free resources; make the timeout a configurable
constant or field (e.g., flushTimeout) on RetrievalStatsLogStore for
testability.

In `@internal/worker/handlers_data.go`:
- Around line 438-443: The handler currently swallows time.Parse errors for the
"since" query (code using sinceStr := r.URL.Query().Get("since") and
time.Parse(time.RFC3339, sinceStr)), causing invalid dates to be treated as "no
since" and returning all-time stats; change this to validate the parse and
return an HTTP 400 with a clear message when parsing fails (mirror the behavior
in handleGetSearchAnalytics which checks the parse and responds with a 400),
i.e., if time.Parse returns an error for sinceStr, call the same error response
path instead of ignoring the error.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 48b974ea-2fd8-4492-97bc-ee575d1707bf

📥 Commits

Reviewing files that changed from the base of the PR and between 00d4a38 and 9087c02.

📒 Files selected for processing (5)
  • internal/db/gorm/retrieval_stats_log_store.go
  • internal/worker/handlers_data.go
  • internal/worker/handlers_maintenance.go
  • internal/worker/service.go
  • ui/src/utils/api.ts

@thebtf thebtf merged commit 25b1a37 into main Mar 20, 2026
2 checks passed
@thebtf thebtf deleted the feat/retrieval-stats-logger branch March 20, 2026 01:00
Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a robust mechanism for persisting retrieval statistics using a batched flushing approach, which is an excellent design for performance. The integration into the existing service, including API updates, maintenance tasks, and a fallback to in-memory stats, is well-thought-out. My review provides suggestions to enhance maintainability by replacing magic values with constants, improving error handling for client-provided parameters, and simplifying redundant code.

Comment on lines +153 to +168
switch r.EventType {
case "search_request":
stats.SearchRequests = r.Total
stats.TotalRequests += r.Total
case "context_injection":
stats.ContextInjections = r.Total
stats.TotalRequests += r.Total
case "observations_served":
stats.ObservationsServed = r.Total
case "stale_excluded":
stats.StaleExcluded = r.Total
case "fresh_count":
stats.FreshCount = r.Total
case "duplicates_removed":
stats.DuplicatesRemoved = r.Total
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The event type strings like "search_request" are hardcoded. This makes the code prone to typos and harder to maintain. It's a good practice to define these as constants and export them so they can be used consistently across packages. For example:

const (
    RetrievalStatSearchRequest    = "search_request"
    RetrievalStatContextInjection = "context_injection"
    // ... and so on for other event types
)

These constants can then be used here and in internal/worker/service.go.

Comment on lines +439 to +443
if sinceStr := r.URL.Query().Get("since"); sinceStr != "" {
if t, err := time.Parse(time.RFC3339, sinceStr); err == nil {
since = t
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

If parsing the since timestamp fails, the error is silently ignored. This results in falling back to all-time stats, which can be confusing for the user who intended to filter by a time range. It would be better to log a warning when parsing fails to make this behavior explicit and aid in debugging client-side issues.

Suggested change
if sinceStr := r.URL.Query().Get("since"); sinceStr != "" {
if t, err := time.Parse(time.RFC3339, sinceStr); err == nil {
since = t
}
}
if t, err := time.Parse(time.RFC3339, sinceStr); err != nil {
log.Warn().Err(err).Str("since", sinceStr).Msg("invalid 'since' parameter format, ignoring")
} else {
since = t
}

if rsStore == nil {
return
}
deleted, err := rsStore.Cleanup(context.Background(), 90*24*time.Hour)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The retention period 90*24*time.Hour is a magic number. Defining this as a named constant at the package level would improve readability and make it easier to change in the future. For example: const retrievalStatsLogRetention = 90 * 24 * time.Hour.

Comment on lines +1879 to +1897
if logStore != nil {
if isSearch {
logStore.LogEvent(project, "search_request", 1)
} else {
logStore.LogEvent(project, "context_injection", 1)
}
if served > 0 {
logStore.LogEvent(project, "observations_served", int(served))
}
if staleExcluded > 0 {
logStore.LogEvent(project, "stale_excluded", int(staleExcluded))
}
if freshCount > 0 {
logStore.LogEvent(project, "fresh_count", int(freshCount))
}
if duplicatesRemoved > 0 {
logStore.LogEvent(project, "duplicates_removed", int(duplicatesRemoved))
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

This block can be improved in two ways:

  1. The event type strings are hardcoded. It's better to use constants (as suggested in retrieval_stats_log_store.go) for consistency and maintainability.
  2. The if ... > 0 checks are redundant because LogEvent already handles count <= 0.

By addressing both, the code becomes cleaner and more robust.

    if logStore != nil {
        if isSearch {
            logStore.LogEvent(project, gorm.RetrievalStatSearchRequest, 1)
        } else {
            logStore.LogEvent(project, gorm.RetrievalStatContextInjection, 1)
        }
        logStore.LogEvent(project, gorm.RetrievalStatObservationsServed, int(served))
        logStore.LogEvent(project, gorm.RetrievalStatStaleExcluded, int(staleExcluded))
        logStore.LogEvent(project, gorm.RetrievalStatFreshCount, int(freshCount))
        logStore.LogEvent(project, gorm.RetrievalStatDuplicatesRemoved, int(duplicatesRemoved))
    }

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a robust mechanism for persisting retrieval statistics using a batched background flusher, which is a significant enhancement for analytics. The implementation is solid, with a new API endpoint for querying time-ranged stats and a corresponding cleanup job. My review includes suggestions to further improve maintainability and API robustness by using constants for shared string literals, refactoring for code clarity, and adding stricter validation for API parameters.

Comment on lines +75 to +98
batch := make([]RetrievalStatsLogEntry, 0, retrievalStatsFlushSize)

for {
select {
case entry, ok := <-s.ch:
if !ok {
// Channel closed — flush remaining and exit.
if len(batch) > 0 {
s.flush(batch)
}
return
}
batch = append(batch, entry)
if len(batch) >= retrievalStatsFlushSize {
s.flush(batch)
batch = batch[:0]
}
case <-ticker.C:
if len(batch) > 0 {
s.flush(batch)
batch = batch[:0]
}
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The flusher function has some duplicated code for flushing the batch. You can refactor this to improve maintainability by extracting the flushing logic into a local helper function.

	batch := make([]RetrievalStatsLogEntry, 0, retrievalStatsFlushSize)

	flushBatch := func() {
		if len(batch) > 0 {
			s.flush(batch)
			batch = batch[:0]
		}
	}

	for {
		select {
		case entry, ok := <-s.ch:
			if !ok {
				// Channel closed — flush remaining and exit.
				flushBatch()
				return
			}
			batch = append(batch, entry)
			if len(batch) >= retrievalStatsFlushSize {
				flushBatch()
			}
		case <-ticker.C:
			flushBatch()
		}
	}

Comment on lines +153 to +168
switch r.EventType {
case "search_request":
stats.SearchRequests = r.Total
stats.TotalRequests += r.Total
case "context_injection":
stats.ContextInjections = r.Total
stats.TotalRequests += r.Total
case "observations_served":
stats.ObservationsServed = r.Total
case "stale_excluded":
stats.StaleExcluded = r.Total
case "fresh_count":
stats.FreshCount = r.Total
case "duplicates_removed":
stats.DuplicatesRemoved = r.Total
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The event type strings like "search_request" are hardcoded here and also in internal/worker/service.go. This is prone to typos and makes maintenance harder. It would be better to define these as exported constants in this package and use them consistently.

Example:

const (
    EventTypeSearchRequest    = "search_request"
    EventTypeContextInjection = "context_injection"
    // ... and so on for other event types
)

Comment on lines +439 to +443
if sinceStr := r.URL.Query().Get("since"); sinceStr != "" {
if t, err := time.Parse(time.RFC3339, sinceStr); err == nil {
since = t
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

If parsing the since parameter fails, the error is silently ignored, and the endpoint falls back to returning all-time stats. This could be confusing for API users who provide an invalid timestamp and expect an error or a filtered response. It's better to return a 400 Bad Request error to provide clear feedback.

		if sinceStr := r.URL.Query().Get("since"); sinceStr != "" {
			t, err := time.Parse(time.RFC3339, sinceStr)
			if err != nil {
				http.Error(w, "invalid 'since' parameter format, must be RFC3339", http.StatusBadRequest)
				return
			}
			since = t
		}

if rsStore == nil {
return
}
deleted, err := rsStore.Cleanup(context.Background(), 90*24*time.Hour)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The retention period 90*24*time.Hour is a magic number. It's also used for cleaning up search query logs. To improve readability and maintainability, you should define this value as a constant, for example const analyticsLogRetentionPeriod = 90 * 24 * time.Hour, and use it in both places.

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