feat: add desktop bug report collection
This commit is contained in:
@@ -0,0 +1,367 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
|
||||
"github.com/geo-platform/tenant-api/internal/ops/domain"
|
||||
)
|
||||
|
||||
var ErrDesktopBugReportNotFound = errors.New("desktop bug report not found")
|
||||
|
||||
type DesktopBugReportRepository struct {
|
||||
pool *pgxpool.Pool
|
||||
}
|
||||
|
||||
func NewDesktopBugReportRepository(pool *pgxpool.Pool) *DesktopBugReportRepository {
|
||||
return &DesktopBugReportRepository{pool: pool}
|
||||
}
|
||||
|
||||
type CreateDesktopBugReportInput struct {
|
||||
ID string
|
||||
ReportType string
|
||||
Status string
|
||||
Severity string
|
||||
TenantID *int64
|
||||
WorkspaceID *int64
|
||||
UserID *int64
|
||||
DesktopClientID *string
|
||||
CrashGUID *string
|
||||
CrashReportID *string
|
||||
ProcessType *string
|
||||
ElectronVersion *string
|
||||
AppVersion *string
|
||||
ProductName *string
|
||||
Platform *string
|
||||
OSArch *string
|
||||
DeviceName *string
|
||||
Title string
|
||||
Description *string
|
||||
DumpObjectKey *string
|
||||
DumpFileName *string
|
||||
DumpSizeBytes *int64
|
||||
DumpSHA256 *string
|
||||
FormFields []byte
|
||||
RuntimeSnapshot []byte
|
||||
AppLogTail *string
|
||||
UploaderIP *string
|
||||
UserAgent *string
|
||||
OccurredAt time.Time
|
||||
}
|
||||
|
||||
type DesktopBugReportFilter struct {
|
||||
Keyword string
|
||||
Status string
|
||||
Severity string
|
||||
ReportType string
|
||||
Platform string
|
||||
Process string
|
||||
TenantID *int64
|
||||
ClientID string
|
||||
Limit int
|
||||
Offset int
|
||||
}
|
||||
|
||||
type UpdateDesktopBugReportStateInput struct {
|
||||
Status string
|
||||
Severity string
|
||||
ResolutionNote *string
|
||||
}
|
||||
|
||||
const desktopBugReportSelectColumns = `
|
||||
br.id::text,
|
||||
br.report_type,
|
||||
br.status,
|
||||
br.severity,
|
||||
br.tenant_id,
|
||||
t.name,
|
||||
br.workspace_id,
|
||||
w.name,
|
||||
br.user_id,
|
||||
u.name,
|
||||
u.phone,
|
||||
u.email,
|
||||
br.desktop_client_id::text,
|
||||
br.crash_guid,
|
||||
br.crash_report_id,
|
||||
br.process_type,
|
||||
br.electron_version,
|
||||
br.app_version,
|
||||
br.product_name,
|
||||
br.platform,
|
||||
br.os_arch,
|
||||
br.device_name,
|
||||
br.title,
|
||||
br.description,
|
||||
br.dump_object_key,
|
||||
br.dump_file_name,
|
||||
br.dump_size_bytes,
|
||||
br.dump_sha256,
|
||||
br.form_fields,
|
||||
br.runtime_snapshot,
|
||||
br.app_log_tail,
|
||||
br.resolution_note,
|
||||
br.uploader_ip,
|
||||
br.user_agent,
|
||||
br.occurred_at,
|
||||
br.resolved_at,
|
||||
br.created_at,
|
||||
br.updated_at`
|
||||
|
||||
const desktopBugReportFromClause = `
|
||||
FROM desktop_bug_reports br
|
||||
LEFT JOIN tenants t ON t.id = br.tenant_id
|
||||
LEFT JOIN workspaces w ON w.id = br.workspace_id
|
||||
LEFT JOIN users u ON u.id = br.user_id`
|
||||
|
||||
func (r *DesktopBugReportRepository) Create(ctx context.Context, in CreateDesktopBugReportInput) (*domain.DesktopBugReport, error) {
|
||||
var id string
|
||||
if err := r.pool.QueryRow(ctx, `
|
||||
INSERT INTO desktop_bug_reports (
|
||||
id, report_type, status, severity, tenant_id, workspace_id, user_id,
|
||||
desktop_client_id, crash_guid, crash_report_id, process_type, electron_version,
|
||||
app_version, product_name, platform, os_arch, device_name, title, description,
|
||||
dump_object_key, dump_file_name, dump_size_bytes, dump_sha256, form_fields,
|
||||
runtime_snapshot, app_log_tail, uploader_ip, user_agent, occurred_at
|
||||
)
|
||||
VALUES (
|
||||
$1::uuid, $2, $3, $4, $5, $6, $7,
|
||||
$8::uuid, $9, $10, $11, $12,
|
||||
$13, $14, $15, $16, $17, $18, $19,
|
||||
$20, $21, $22, $23, $24::jsonb,
|
||||
$25::jsonb, $26, $27, $28, $29
|
||||
)
|
||||
RETURNING id::text`,
|
||||
in.ID, in.ReportType, in.Status, in.Severity, in.TenantID, in.WorkspaceID, in.UserID,
|
||||
in.DesktopClientID, in.CrashGUID, in.CrashReportID, in.ProcessType, in.ElectronVersion,
|
||||
in.AppVersion, in.ProductName, in.Platform, in.OSArch, in.DeviceName, in.Title, in.Description,
|
||||
in.DumpObjectKey, in.DumpFileName, in.DumpSizeBytes, in.DumpSHA256, string(defaultJSON(in.FormFields)),
|
||||
string(defaultJSON(in.RuntimeSnapshot)), in.AppLogTail, in.UploaderIP, in.UserAgent, in.OccurredAt).Scan(&id); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return r.GetByID(ctx, id)
|
||||
}
|
||||
|
||||
func (r *DesktopBugReportRepository) List(ctx context.Context, f DesktopBugReportFilter) ([]domain.DesktopBugReport, int64, error) {
|
||||
args := []any{}
|
||||
where := "1=1"
|
||||
|
||||
if f.Keyword != "" {
|
||||
args = append(args, "%"+f.Keyword+"%")
|
||||
where += fmt.Sprintf(` AND (
|
||||
br.id::text ILIKE $%d OR
|
||||
COALESCE(br.title, '') ILIKE $%d OR
|
||||
COALESCE(br.description, '') ILIKE $%d OR
|
||||
COALESCE(br.crash_guid, '') ILIKE $%d OR
|
||||
COALESCE(br.crash_report_id, '') ILIKE $%d OR
|
||||
COALESCE(br.dump_object_key, '') ILIKE $%d OR
|
||||
COALESCE(br.device_name, '') ILIKE $%d OR
|
||||
COALESCE(t.name, '') ILIKE $%d OR
|
||||
COALESCE(w.name, '') ILIKE $%d OR
|
||||
COALESCE(u.name, '') ILIKE $%d OR
|
||||
COALESCE(u.phone, '') ILIKE $%d OR
|
||||
COALESCE(u.email, '') ILIKE $%d
|
||||
)`, len(args), len(args), len(args), len(args), len(args), len(args), len(args), len(args), len(args), len(args), len(args), len(args))
|
||||
}
|
||||
if f.Status != "" {
|
||||
args = append(args, f.Status)
|
||||
where += fmt.Sprintf(" AND br.status = $%d", len(args))
|
||||
}
|
||||
if f.Severity != "" {
|
||||
args = append(args, f.Severity)
|
||||
where += fmt.Sprintf(" AND br.severity = $%d", len(args))
|
||||
}
|
||||
if f.ReportType != "" {
|
||||
args = append(args, f.ReportType)
|
||||
where += fmt.Sprintf(" AND br.report_type = $%d", len(args))
|
||||
}
|
||||
if f.Platform != "" {
|
||||
args = append(args, f.Platform)
|
||||
where += fmt.Sprintf(" AND br.platform = $%d", len(args))
|
||||
}
|
||||
if f.Process != "" {
|
||||
args = append(args, f.Process)
|
||||
where += fmt.Sprintf(" AND br.process_type = $%d", len(args))
|
||||
}
|
||||
if f.TenantID != nil {
|
||||
args = append(args, *f.TenantID)
|
||||
where += fmt.Sprintf(" AND br.tenant_id = $%d", len(args))
|
||||
}
|
||||
if f.ClientID != "" {
|
||||
args = append(args, f.ClientID)
|
||||
where += fmt.Sprintf(" AND br.desktop_client_id = $%d::uuid", len(args))
|
||||
}
|
||||
|
||||
var total int64
|
||||
if err := r.pool.QueryRow(ctx, "SELECT COUNT(*) "+desktopBugReportFromClause+" WHERE "+where, args...).Scan(&total); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
limit := f.Limit
|
||||
if limit <= 0 || limit > 200 {
|
||||
limit = 20
|
||||
}
|
||||
offset := f.Offset
|
||||
if offset < 0 {
|
||||
offset = 0
|
||||
}
|
||||
args = append(args, limit, offset)
|
||||
limitArg := len(args) - 1
|
||||
offsetArg := len(args)
|
||||
|
||||
rows, err := r.pool.Query(ctx, fmt.Sprintf(`
|
||||
SELECT %s
|
||||
%s
|
||||
WHERE %s
|
||||
ORDER BY br.created_at DESC, br.id DESC
|
||||
LIMIT $%d OFFSET $%d`, desktopBugReportSelectColumns, desktopBugReportFromClause, where, limitArg, offsetArg), args...)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
items := make([]domain.DesktopBugReport, 0, limit)
|
||||
for rows.Next() {
|
||||
item, scanErr := scanDesktopBugReport(rows)
|
||||
if scanErr != nil {
|
||||
return nil, 0, scanErr
|
||||
}
|
||||
items = append(items, *item)
|
||||
}
|
||||
return items, total, rows.Err()
|
||||
}
|
||||
|
||||
func (r *DesktopBugReportRepository) GetByID(ctx context.Context, id string) (*domain.DesktopBugReport, error) {
|
||||
return scanDesktopBugReport(r.pool.QueryRow(ctx, `
|
||||
SELECT `+desktopBugReportSelectColumns+`
|
||||
`+desktopBugReportFromClause+`
|
||||
WHERE br.id = $1::uuid`, id))
|
||||
}
|
||||
|
||||
func (r *DesktopBugReportRepository) UpdateState(ctx context.Context, id string, in UpdateDesktopBugReportStateInput) (*domain.DesktopBugReport, error) {
|
||||
var updatedID string
|
||||
if err := r.pool.QueryRow(ctx, `
|
||||
UPDATE desktop_bug_reports br
|
||||
SET status = $2,
|
||||
severity = $3,
|
||||
resolution_note = $4,
|
||||
resolved_at = CASE
|
||||
WHEN $2 IN ('resolved', 'ignored') AND resolved_at IS NULL THEN NOW()
|
||||
WHEN $2 NOT IN ('resolved', 'ignored') THEN NULL
|
||||
ELSE resolved_at
|
||||
END,
|
||||
updated_at = NOW()
|
||||
WHERE br.id = $1::uuid
|
||||
RETURNING br.id::text`,
|
||||
id, in.Status, in.Severity, in.ResolutionNote).Scan(&updatedID); err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return nil, ErrDesktopBugReportNotFound
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return r.GetByID(ctx, updatedID)
|
||||
}
|
||||
|
||||
func scanDesktopBugReport(row pgx.Row) (*domain.DesktopBugReport, error) {
|
||||
var item domain.DesktopBugReport
|
||||
var tenantID, workspaceID, userID, dumpSize pgtype.Int8
|
||||
var tenantName, workspaceName, userName, userPhone, userEmail pgtype.Text
|
||||
var desktopClientID, crashGUID, crashReportID, processType, electronVersion, appVersion pgtype.Text
|
||||
var productName, platform, osArch, deviceName, description, dumpObjectKey, dumpFileName pgtype.Text
|
||||
var dumpSHA256, appLogTail, resolutionNote, uploaderIP, userAgent pgtype.Text
|
||||
var formFields, runtimeSnapshot []byte
|
||||
var resolvedAt pgtype.Timestamptz
|
||||
|
||||
err := row.Scan(
|
||||
&item.ID,
|
||||
&item.ReportType,
|
||||
&item.Status,
|
||||
&item.Severity,
|
||||
&tenantID,
|
||||
&tenantName,
|
||||
&workspaceID,
|
||||
&workspaceName,
|
||||
&userID,
|
||||
&userName,
|
||||
&userPhone,
|
||||
&userEmail,
|
||||
&desktopClientID,
|
||||
&crashGUID,
|
||||
&crashReportID,
|
||||
&processType,
|
||||
&electronVersion,
|
||||
&appVersion,
|
||||
&productName,
|
||||
&platform,
|
||||
&osArch,
|
||||
&deviceName,
|
||||
&item.Title,
|
||||
&description,
|
||||
&dumpObjectKey,
|
||||
&dumpFileName,
|
||||
&dumpSize,
|
||||
&dumpSHA256,
|
||||
&formFields,
|
||||
&runtimeSnapshot,
|
||||
&appLogTail,
|
||||
&resolutionNote,
|
||||
&uploaderIP,
|
||||
&userAgent,
|
||||
&item.OccurredAt,
|
||||
&resolvedAt,
|
||||
&item.CreatedAt,
|
||||
&item.UpdatedAt,
|
||||
)
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return nil, ErrDesktopBugReportNotFound
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
item.TenantID = int64PtrFromPg(tenantID)
|
||||
item.TenantName = stringPtrFromPg(tenantName)
|
||||
item.WorkspaceID = int64PtrFromPg(workspaceID)
|
||||
item.WorkspaceName = stringPtrFromPg(workspaceName)
|
||||
item.UserID = int64PtrFromPg(userID)
|
||||
item.UserName = stringPtrFromPg(userName)
|
||||
item.UserPhone = stringPtrFromPg(userPhone)
|
||||
item.UserEmail = stringPtrFromPg(userEmail)
|
||||
item.DesktopClientID = stringPtrFromPg(desktopClientID)
|
||||
item.CrashGUID = stringPtrFromPg(crashGUID)
|
||||
item.CrashReportID = stringPtrFromPg(crashReportID)
|
||||
item.ProcessType = stringPtrFromPg(processType)
|
||||
item.ElectronVersion = stringPtrFromPg(electronVersion)
|
||||
item.AppVersion = stringPtrFromPg(appVersion)
|
||||
item.ProductName = stringPtrFromPg(productName)
|
||||
item.Platform = stringPtrFromPg(platform)
|
||||
item.OSArch = stringPtrFromPg(osArch)
|
||||
item.DeviceName = stringPtrFromPg(deviceName)
|
||||
item.Description = stringPtrFromPg(description)
|
||||
item.DumpObjectKey = stringPtrFromPg(dumpObjectKey)
|
||||
item.DumpFileName = stringPtrFromPg(dumpFileName)
|
||||
item.DumpSizeBytes = int64PtrFromPg(dumpSize)
|
||||
item.DumpSHA256 = stringPtrFromPg(dumpSHA256)
|
||||
item.FormFields = json.RawMessage(defaultJSON(formFields))
|
||||
item.RuntimeSnapshot = json.RawMessage(defaultJSON(runtimeSnapshot))
|
||||
item.AppLogTail = stringPtrFromPg(appLogTail)
|
||||
item.ResolutionNote = stringPtrFromPg(resolutionNote)
|
||||
item.UploaderIP = stringPtrFromPg(uploaderIP)
|
||||
item.UserAgent = stringPtrFromPg(userAgent)
|
||||
item.ResolvedAt = timePtrFromPg(resolvedAt)
|
||||
return &item, nil
|
||||
}
|
||||
|
||||
func defaultJSON(value []byte) []byte {
|
||||
if len(value) == 0 || !json.Valid(value) {
|
||||
return []byte("{}")
|
||||
}
|
||||
return value
|
||||
}
|
||||
Reference in New Issue
Block a user