From c36e30f3a9496e5ddf40c9493e3d5c364ce6c645 Mon Sep 17 00:00:00 2001 From: Acho Arnold Date: Sun, 19 Jul 2026 15:00:43 +0300 Subject: [PATCH 01/88] feat: add message thread read receipts (#956) * docs: add read receipts design Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: bf00a0ac-e11f-4015-b295-3cdd9b491229 * docs: add read receipts implementation plan Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: bf00a0ac-e11f-4015-b295-3cdd9b491229 * docs: add read receipts integration coverage Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: bf00a0ac-e11f-4015-b295-3cdd9b491229 * feat(api): add thread read state contract Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: bf00a0ac-e11f-4015-b295-3cdd9b491229 * refactor(api): make thread updates atomic Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: bf00a0ac-e11f-4015-b295-3cdd9b491229 * feat(api): track unread inbound threads Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: bf00a0ac-e11f-4015-b295-3cdd9b491229 * docs(api): publish thread read state Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: bf00a0ac-e11f-4015-b295-3cdd9b491229 * style(api): format thread handler test Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: bf00a0ac-e11f-4015-b295-3cdd9b491229 * feat(web): persist opened threads as read Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: bf00a0ac-e11f-4015-b295-3cdd9b491229 * chore(web): keep read store change scoped Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: bf00a0ac-e11f-4015-b295-3cdd9b491229 * fix(web): preserve thread update failures Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: bf00a0ac-e11f-4015-b295-3cdd9b491229 * feat(web): highlight unread message threads Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: bf00a0ac-e11f-4015-b295-3cdd9b491229 * test: cover message thread read receipts Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: bf00a0ac-e11f-4015-b295-3cdd9b491229 * test: preserve event timestamp precision Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: bf00a0ac-e11f-4015-b295-3cdd9b491229 * fix(api): persist new unread threads Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: bf00a0ac-e11f-4015-b295-3cdd9b491229 * style(api): apply gofumpt to handler test Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: bf00a0ac-e11f-4015-b295-3cdd9b491229 * test(api): update listener service setup Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: bf00a0ac-e11f-4015-b295-3cdd9b491229 * fix: harden read receipt synchronization Avoid duplicate realtime read updates and the post-write reload race. Retry asynchronous phone-key binding in integration tests. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: bf00a0ac-e11f-4015-b295-3cdd9b491229 * docs: normalize plan code indentation Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: bf00a0ac-e11f-4015-b295-3cdd9b491229 * fix(api): return updated thread atomically Use PostgreSQL RETURNING so concurrent activity cannot make status update responses stale. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: bf00a0ac-e11f-4015-b295-3cdd9b491229 * refactor(api): update activity atomically Fold the unread watermark into the activity update so inbound events use one database query. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: bf00a0ac-e11f-4015-b295-3cdd9b491229 * style: fix thread ui for unread threads --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- api/docs/docs.go | 41 +- api/docs/swagger.json | 41 +- api/docs/swagger.yaml | 30 +- api/pkg/entities/message_thread.go | 2 + api/pkg/entities/message_thread_test.go | 25 + api/pkg/handlers/message_thread_handler.go | 6 +- .../handlers/message_thread_handler_test.go | 110 ++ api/pkg/listeners/message_thread_listener.go | 44 +- .../listeners/message_thread_listener_test.go | 72 + .../read_receipts_test_helpers_test.go | 66 + api/pkg/listeners/websocket_listener.go | 16 + api/pkg/listeners/websocket_listener_test.go | 18 + .../gorm_message_thread_repository.go | 123 +- .../gorm_message_thread_repository_test.go | 204 ++ .../repositories/message_thread_repository.go | 37 +- .../requests/message_thread_update_request.go | 4 +- .../message_thread_update_request_test.go | 25 + api/pkg/responses/message_thead_responses.go | 6 + api/pkg/services/message_thread_service.go | 63 +- .../services/message_thread_service_test.go | 214 ++ .../message_thread_handler_validator.go | 10 +- .../message_thread_handler_validator_test.go | 34 + .../plans/2026-07-18-read-receipts.md | 1760 +++++++++++++++++ .../specs/2026-07-18-read-receipts-design.md | 286 +++ tests/README.md | 1 + tests/read_receipts_test.go | 206 ++ web/app/components/MessageThread.vue | 9 +- web/app/pages/threads/[id]/index.vue | 23 +- web/app/stores/threads.ts | 40 + web/shared/types/api.ts | 14 +- 30 files changed, 3462 insertions(+), 68 deletions(-) create mode 100644 api/pkg/entities/message_thread_test.go create mode 100644 api/pkg/handlers/message_thread_handler_test.go create mode 100644 api/pkg/listeners/message_thread_listener_test.go create mode 100644 api/pkg/listeners/read_receipts_test_helpers_test.go create mode 100644 api/pkg/listeners/websocket_listener_test.go create mode 100644 api/pkg/repositories/gorm_message_thread_repository_test.go create mode 100644 api/pkg/requests/message_thread_update_request_test.go create mode 100644 api/pkg/validators/message_thread_handler_validator_test.go create mode 100644 docs/superpowers/plans/2026-07-18-read-receipts.md create mode 100644 docs/superpowers/specs/2026-07-18-read-receipts-design.md create mode 100644 tests/read_receipts_test.go diff --git a/api/docs/docs.go b/api/docs/docs.go index c7fcd1145..8cd408cb1 100644 --- a/api/docs/docs.go +++ b/api/docs/docs.go @@ -871,7 +871,7 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/responses.PhoneResponse" + "$ref": "#/definitions/responses.MessageThreadResponse" } }, "400": { @@ -886,6 +886,12 @@ const docTemplate = `{ "$ref": "#/definitions/responses.Unauthorized" } }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/responses.NotFound" + } + }, "422": { "description": "Unprocessable Entity", "schema": { @@ -3691,6 +3697,7 @@ const docTemplate = `{ "created_at", "id", "is_archived", + "is_read", "last_message_content", "last_message_id", "order_timestamp", @@ -3720,6 +3727,10 @@ const docTemplate = `{ "type": "boolean", "example": false }, + "is_read": { + "type": "boolean", + "example": true + }, "last_message_content": { "type": "string", "example": "This is a sample message content" @@ -4388,13 +4399,14 @@ const docTemplate = `{ }, "requests.MessageThreadUpdate": { "type": "object", - "required": [ - "is_archived" - ], "properties": { "is_archived": { "type": "boolean", "example": true + }, + "is_read": { + "type": "boolean", + "example": true } } }, @@ -4904,6 +4916,27 @@ const docTemplate = `{ } } }, + "responses.MessageThreadResponse": { + "type": "object", + "required": [ + "data", + "message", + "status" + ], + "properties": { + "data": { + "$ref": "#/definitions/entities.MessageThread" + }, + "message": { + "type": "string", + "example": "Request handled successfully" + }, + "status": { + "type": "string", + "example": "success" + } + } + }, "responses.MessageThreadsResponse": { "type": "object", "required": [ diff --git a/api/docs/swagger.json b/api/docs/swagger.json index 25db6fc31..ac9c12c98 100644 --- a/api/docs/swagger.json +++ b/api/docs/swagger.json @@ -868,7 +868,7 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/responses.PhoneResponse" + "$ref": "#/definitions/responses.MessageThreadResponse" } }, "400": { @@ -883,6 +883,12 @@ "$ref": "#/definitions/responses.Unauthorized" } }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/responses.NotFound" + } + }, "422": { "description": "Unprocessable Entity", "schema": { @@ -3688,6 +3694,7 @@ "created_at", "id", "is_archived", + "is_read", "last_message_content", "last_message_id", "order_timestamp", @@ -3717,6 +3724,10 @@ "type": "boolean", "example": false }, + "is_read": { + "type": "boolean", + "example": true + }, "last_message_content": { "type": "string", "example": "This is a sample message content" @@ -4385,13 +4396,14 @@ }, "requests.MessageThreadUpdate": { "type": "object", - "required": [ - "is_archived" - ], "properties": { "is_archived": { "type": "boolean", "example": true + }, + "is_read": { + "type": "boolean", + "example": true } } }, @@ -4901,6 +4913,27 @@ } } }, + "responses.MessageThreadResponse": { + "type": "object", + "required": [ + "data", + "message", + "status" + ], + "properties": { + "data": { + "$ref": "#/definitions/entities.MessageThread" + }, + "message": { + "type": "string", + "example": "Request handled successfully" + }, + "status": { + "type": "string", + "example": "success" + } + } + }, "responses.MessageThreadsResponse": { "type": "object", "required": [ diff --git a/api/docs/swagger.yaml b/api/docs/swagger.yaml index 892c9ae45..ddc6ae700 100644 --- a/api/docs/swagger.yaml +++ b/api/docs/swagger.yaml @@ -319,6 +319,9 @@ definitions: is_archived: example: false type: boolean + is_read: + example: true + type: boolean last_message_content: example: This is a sample message content type: string @@ -346,6 +349,7 @@ definitions: - created_at - id - is_archived + - is_read - last_message_content - last_message_id - order_timestamp @@ -853,8 +857,9 @@ definitions: is_archived: example: true type: boolean - required: - - is_archived + is_read: + example: true + type: boolean type: object requests.PhoneAPIKeyStoreRequest: properties: @@ -1227,6 +1232,21 @@ definitions: - message - status type: object + responses.MessageThreadResponse: + properties: + data: + $ref: '#/definitions/entities.MessageThread' + message: + example: Request handled successfully + type: string + status: + example: success + type: string + required: + - data + - message + - status + type: object responses.MessageThreadsResponse: properties: data: @@ -2182,7 +2202,7 @@ paths: "200": description: OK schema: - $ref: '#/definitions/responses.PhoneResponse' + $ref: '#/definitions/responses.MessageThreadResponse' "400": description: Bad Request schema: @@ -2191,6 +2211,10 @@ paths: description: Unauthorized schema: $ref: '#/definitions/responses.Unauthorized' + "404": + description: Not Found + schema: + $ref: '#/definitions/responses.NotFound' "422": description: Unprocessable Entity schema: diff --git a/api/pkg/entities/message_thread.go b/api/pkg/entities/message_thread.go index 2eb1e2d33..3766acc77 100644 --- a/api/pkg/entities/message_thread.go +++ b/api/pkg/entities/message_thread.go @@ -12,6 +12,8 @@ type MessageThread struct { Owner string `json:"owner" example:"+18005550199"` Contact string `json:"contact" example:"+18005550100"` IsArchived bool `json:"is_archived" example:"false"` + IsRead bool `json:"is_read" gorm:"not null;default:true" example:"true"` + LastReadAt time.Time `json:"-" gorm:"not null;default:CURRENT_TIMESTAMP"` UserID UserID `json:"user_id" example:"WB7DRDWrJZRGbYrv2CKGkqbzvqdC"` Color string `json:"color" example:"indigo"` Status MessageStatus `json:"status" example:"PENDING"` diff --git a/api/pkg/entities/message_thread_test.go b/api/pkg/entities/message_thread_test.go new file mode 100644 index 000000000..1409dff29 --- /dev/null +++ b/api/pkg/entities/message_thread_test.go @@ -0,0 +1,25 @@ +package entities + +import ( + "reflect" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestMessageThreadReadFieldsHaveBackwardCompatibleDefaults(t *testing.T) { + threadType := reflect.TypeOf(MessageThread{}) + + isRead, ok := threadType.FieldByName("IsRead") + require.True(t, ok) + assert.Contains(t, isRead.Tag.Get("gorm"), "not null") + assert.Contains(t, isRead.Tag.Get("gorm"), "default:true") + assert.Equal(t, "is_read", isRead.Tag.Get("json")) + + lastReadAt, ok := threadType.FieldByName("LastReadAt") + require.True(t, ok) + assert.Contains(t, lastReadAt.Tag.Get("gorm"), "not null") + assert.Contains(t, lastReadAt.Tag.Get("gorm"), "default:CURRENT_TIMESTAMP") + assert.Equal(t, "-", lastReadAt.Tag.Get("json")) +} diff --git a/api/pkg/handlers/message_thread_handler.go b/api/pkg/handlers/message_thread_handler.go index 1a00f7737..9520b6634 100644 --- a/api/pkg/handlers/message_thread_handler.go +++ b/api/pkg/handlers/message_thread_handler.go @@ -100,9 +100,10 @@ func (h *MessageThreadHandler) Index(c fiber.Ctx) error { // @Produce json // @Param messageThreadID path string true "ID of the message thread" default(32343a19-da5e-4b1b-a767-3298a73703ca) // @Param payload body requests.MessageThreadUpdate true "Payload of message thread details to update" -// @Success 200 {object} responses.PhoneResponse +// @Success 200 {object} responses.MessageThreadResponse // @Failure 400 {object} responses.BadRequest // @Failure 401 {object} responses.Unauthorized +// @Failure 404 {object} responses.NotFound // @Failure 422 {object} responses.UnprocessableEntity // @Failure 500 {object} responses.InternalServerError // @Router /message-threads/{messageThreadID} [put] @@ -123,6 +124,9 @@ func (h *MessageThreadHandler) Update(c fiber.Ctx) error { } thread, err := h.service.UpdateStatus(ctx, request.ToUpdateParams(h.userIDFomContext(c))) + if stacktrace.GetCode(err) == repositories.ErrCodeNotFound { + return h.responseNotFound(c, fmt.Sprintf("cannot find message thread with ID [%s]", request.MessageThreadID)) + } if err != nil { ctxLogger.Error(stacktrace.Propagate(err, "cannot update message thread with params [%+#v]", request)) return h.responseInternalServerError(c) diff --git a/api/pkg/handlers/message_thread_handler_test.go b/api/pkg/handlers/message_thread_handler_test.go new file mode 100644 index 000000000..ab2d75962 --- /dev/null +++ b/api/pkg/handlers/message_thread_handler_test.go @@ -0,0 +1,110 @@ +package handlers + +import ( + "bytes" + "context" + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/NdoleStudio/httpsms/pkg/entities" + "github.com/NdoleStudio/httpsms/pkg/middlewares" + "github.com/NdoleStudio/httpsms/pkg/repositories" + "github.com/NdoleStudio/httpsms/pkg/services" + "github.com/NdoleStudio/httpsms/pkg/telemetry" + "github.com/NdoleStudio/httpsms/pkg/validators" + "github.com/gofiber/fiber/v3" + "github.com/google/uuid" + "github.com/palantir/stacktrace" + "github.com/stretchr/testify/require" + "go.opentelemetry.io/otel/trace" + "gorm.io/gorm" +) + +type messageThreadHandlerRepositoryStub struct{} + +func (stub *messageThreadHandlerRepositoryStub) Store(context.Context, *entities.MessageThread) error { + return nil +} + +func (stub *messageThreadHandlerRepositoryStub) UpdateActivity(context.Context, repositories.MessageThreadActivityUpdate) error { + return nil +} + +func (stub *messageThreadHandlerRepositoryStub) UpdateStatus(context.Context, entities.UserID, uuid.UUID, repositories.MessageThreadStatusUpdate) (*entities.MessageThread, error) { + return nil, stacktrace.PropagateWithCode(gorm.ErrRecordNotFound, repositories.ErrCodeNotFound, "not found") +} + +func (stub *messageThreadHandlerRepositoryStub) LoadByOwnerContact(context.Context, entities.UserID, string, string) (*entities.MessageThread, error) { + return nil, nil +} + +func (stub *messageThreadHandlerRepositoryStub) Load(context.Context, entities.UserID, uuid.UUID) (*entities.MessageThread, error) { + return nil, nil +} + +func (stub *messageThreadHandlerRepositoryStub) Index(context.Context, entities.UserID, string, bool, repositories.IndexParams) (*[]entities.MessageThread, error) { + return nil, nil +} + +func (stub *messageThreadHandlerRepositoryStub) UpdateAfterDeletedMessage(context.Context, repositories.MessageThreadDeletedUpdate) error { + return nil +} + +func (stub *messageThreadHandlerRepositoryStub) Delete(context.Context, entities.UserID, uuid.UUID) error { + return nil +} + +func (stub *messageThreadHandlerRepositoryStub) DeleteAllForUser(context.Context, entities.UserID) error { + return nil +} + +func TestMessageThreadHandlerUpdate_ReturnsNotFoundWhenThreadIsMissing(t *testing.T) { + logger := &messageThreadHandlerNoopLogger{} + tracer := telemetry.NewOtelLogger("test", logger) + service := services.NewMessageThreadService(logger, tracer, &messageThreadHandlerRepositoryStub{}, nil, nil) + handler := NewMessageThreadHandler(logger, tracer, validators.NewMessageThreadHandlerValidator(logger, tracer), service) + + app := fiber.New() + app.Use(func(c fiber.Ctx) error { + c.Locals(middlewares.ContextKeyAuthUserID, entities.AuthContext{ID: entities.UserID("user-id"), Email: "user@example.com"}) + return c.Next() + }) + handler.RegisterRoutes(app) + + messageThreadID := uuid.New() + req := httptest.NewRequest(http.MethodPut, "/v1/message-threads/"+messageThreadID.String(), bytes.NewBufferString(`{"is_read":true}`)) + req.Header.Set("Content-Type", "application/json") + + resp, err := app.Test(req, fiber.TestConfig{Timeout: time.Second}) + + require.NoError(t, err) + require.Equal(t, http.StatusNotFound, resp.StatusCode) + + var payload struct { + Message string `json:"message"` + } + require.NoError(t, json.NewDecoder(resp.Body).Decode(&payload)) + require.Equal(t, "cannot find message thread with ID ["+messageThreadID.String()+"]", payload.Message) +} + +type messageThreadHandlerNoopLogger struct{} + +var _ telemetry.Logger = (*messageThreadHandlerNoopLogger)(nil) + +func (logger *messageThreadHandlerNoopLogger) Error(_ error) {} +func (logger *messageThreadHandlerNoopLogger) WithService(_ string) telemetry.Logger { return logger } + +func (logger *messageThreadHandlerNoopLogger) WithString(_, _ string) telemetry.Logger { return logger } + +func (logger *messageThreadHandlerNoopLogger) WithSpan(_ trace.SpanContext) telemetry.Logger { + return logger +} +func (logger *messageThreadHandlerNoopLogger) Trace(_ string) {} +func (logger *messageThreadHandlerNoopLogger) Info(_ string) {} +func (logger *messageThreadHandlerNoopLogger) Warn(_ error) {} +func (logger *messageThreadHandlerNoopLogger) Debug(_ string) {} +func (logger *messageThreadHandlerNoopLogger) Fatal(_ error) {} +func (logger *messageThreadHandlerNoopLogger) Printf(_ string, _ ...interface{}) {} diff --git a/api/pkg/listeners/message_thread_listener.go b/api/pkg/listeners/message_thread_listener.go index e5f90ca39..47621296e 100644 --- a/api/pkg/listeners/message_thread_listener.go +++ b/api/pkg/listeners/message_thread_listener.go @@ -40,6 +40,7 @@ func NewMessageThreadListener( events.EventTypeMessagePhoneDelivered: l.OnMessagePhoneDelivered, events.EventTypeMessageSendFailed: l.OnMessagePhoneFailed, events.EventTypeMessagePhoneReceived: l.OnMessagePhoneReceived, + events.MessageCallMissed: l.OnMessageCallMissed, events.EventTypeMessageNotificationScheduled: l.onMessageNotificationScheduled, events.EventTypeMessageSendExpired: l.onMessageExpired, events.UserAccountDeleted: l.onUserAccountDeleted, @@ -209,13 +210,15 @@ func (listener *MessageThreadListener) OnMessagePhoneReceived(ctx context.Contex } updateParams := services.MessageThreadUpdateParams{ - Owner: payload.Owner, - Contact: payload.Contact, - Timestamp: payload.Timestamp, - UserID: payload.UserID, - Status: entities.MessageStatusReceived, - Content: payload.Content, - MessageID: payload.MessageID, + Owner: payload.Owner, + Contact: payload.Contact, + Timestamp: payload.Timestamp, + UserID: payload.UserID, + Status: entities.MessageStatusReceived, + Content: payload.Content, + MessageID: payload.MessageID, + MarkAsUnread: true, + EventTimestamp: event.Time(), } if err := listener.service.UpdateThread(ctx, updateParams); err != nil { @@ -225,6 +228,33 @@ func (listener *MessageThreadListener) OnMessagePhoneReceived(ctx context.Contex return nil } +// OnMessageCallMissed handles the events.MessageCallMissed event +func (listener *MessageThreadListener) OnMessageCallMissed(ctx context.Context, event cloudevents.Event) error { + ctx, span := listener.tracer.Start(ctx) + defer span.End() + + var payload events.MessageCallMissedPayload + if err := event.DataAs(&payload); err != nil { + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot decode [%s] into [%T]", event.Data(), payload)) + } + + params := services.MessageThreadUpdateParams{ + Owner: payload.Owner, + Contact: payload.Contact, + UserID: payload.UserID, + Status: entities.MessageStatusReceived, + Timestamp: payload.Timestamp, + Content: "Missed phone call", + MessageID: payload.MessageID, + MarkAsUnread: true, + EventTimestamp: event.Time(), + } + if err := listener.service.UpdateThread(ctx, params); err != nil { + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot update thread for missed call [%s] on event [%s]", payload.MessageID, event.ID())) + } + return nil +} + // onMessageNotificationScheduled handles the events.EventTypeMessageNotificationScheduled event func (listener *MessageThreadListener) onMessageNotificationScheduled(ctx context.Context, event cloudevents.Event) error { ctx, span := listener.tracer.Start(ctx) diff --git a/api/pkg/listeners/message_thread_listener_test.go b/api/pkg/listeners/message_thread_listener_test.go new file mode 100644 index 000000000..39a444cce --- /dev/null +++ b/api/pkg/listeners/message_thread_listener_test.go @@ -0,0 +1,72 @@ +package listeners + +import ( + "context" + "testing" + "time" + + "github.com/NdoleStudio/httpsms/pkg/entities" + "github.com/NdoleStudio/httpsms/pkg/events" + "github.com/NdoleStudio/httpsms/pkg/services" + "github.com/NdoleStudio/httpsms/pkg/telemetry" + cloudevents "github.com/cloudevents/sdk-go/v2" + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestMessageThreadListenerMarksInboundMessageUnread(t *testing.T) { + repository, routes := newMessageThreadListenerForTest() + event := cloudevents.NewEvent() + event.SetID(uuid.NewString()) + event.SetSource("/v1/messages/phone-received") + event.SetType(events.EventTypeMessagePhoneReceived) + event.SetTime(time.Date(2026, 7, 18, 7, 0, 0, 0, time.UTC)) + require.NoError(t, event.SetData(cloudevents.ApplicationJSON, events.MessagePhoneReceivedPayload{ + MessageID: uuid.New(), + UserID: entities.UserID("user-id"), + Owner: "+18005550199", + Contact: "+18005550100", + Content: "hello", + Timestamp: time.Date(2026, 7, 18, 6, 59, 0, 0, time.UTC), + })) + + err := routes[events.EventTypeMessagePhoneReceived](context.Background(), event) + + require.NoError(t, err) + assert.True(t, repository.activity.MarkAsUnread) + assert.Equal(t, event.Time(), repository.activity.EventTimestamp) +} + +func TestMessageThreadListenerMarksMissedCallUnread(t *testing.T) { + repository, routes := newMessageThreadListenerForTest() + event := cloudevents.NewEvent() + event.SetID(uuid.NewString()) + event.SetSource("/v1/messages/call-missed") + event.SetType(events.MessageCallMissed) + event.SetTime(time.Date(2026, 7, 18, 7, 0, 0, 0, time.UTC)) + require.NoError(t, event.SetData(cloudevents.ApplicationJSON, events.MessageCallMissedPayload{ + MessageID: uuid.New(), + UserID: entities.UserID("user-id"), + Owner: "+18005550199", + Contact: "+18005550100", + Timestamp: time.Date(2026, 7, 18, 6, 59, 0, 0, time.UTC), + })) + require.Contains(t, routes, events.MessageCallMissed) + + err := routes[events.MessageCallMissed](context.Background(), event) + + require.NoError(t, err) + assert.True(t, repository.activity.MarkAsUnread) + assert.Equal(t, "Missed phone call", repository.activity.Content) + assert.Equal(t, event.Time(), repository.activity.EventTimestamp) +} + +func newMessageThreadListenerForTest() (*listenerMessageThreadRepository, map[string]events.EventListener) { + repository := &listenerMessageThreadRepository{} + logger := &noopListenerLogger{} + tracer := telemetry.NewOtelLogger("test", logger) + service := services.NewMessageThreadService(logger, tracer, repository, nil, nil) + _, routes := NewMessageThreadListener(logger, tracer, service) + return repository, routes +} diff --git a/api/pkg/listeners/read_receipts_test_helpers_test.go b/api/pkg/listeners/read_receipts_test_helpers_test.go new file mode 100644 index 000000000..60cd27e65 --- /dev/null +++ b/api/pkg/listeners/read_receipts_test_helpers_test.go @@ -0,0 +1,66 @@ +package listeners + +import ( + "context" + + "github.com/NdoleStudio/httpsms/pkg/entities" + "github.com/NdoleStudio/httpsms/pkg/repositories" + "github.com/NdoleStudio/httpsms/pkg/telemetry" + "github.com/google/uuid" + "go.opentelemetry.io/otel/trace" +) + +type noopListenerLogger struct{} + +func (logger *noopListenerLogger) Error(error) {} +func (logger *noopListenerLogger) WithService(string) telemetry.Logger { return logger } +func (logger *noopListenerLogger) WithString(string, string) telemetry.Logger { return logger } +func (logger *noopListenerLogger) WithSpan(trace.SpanContext) telemetry.Logger { return logger } +func (logger *noopListenerLogger) Trace(string) {} +func (logger *noopListenerLogger) Info(string) {} +func (logger *noopListenerLogger) Warn(error) {} +func (logger *noopListenerLogger) Debug(string) {} +func (logger *noopListenerLogger) Fatal(error) {} +func (logger *noopListenerLogger) Printf(string, ...interface{}) {} + +type listenerMessageThreadRepository struct { + activity repositories.MessageThreadActivityUpdate +} + +func (repository *listenerMessageThreadRepository) Store(context.Context, *entities.MessageThread) error { + return nil +} + +func (repository *listenerMessageThreadRepository) UpdateActivity(_ context.Context, params repositories.MessageThreadActivityUpdate) error { + repository.activity = params + return nil +} + +func (repository *listenerMessageThreadRepository) UpdateStatus(_ context.Context, _ entities.UserID, threadID uuid.UUID, _ repositories.MessageThreadStatusUpdate) (*entities.MessageThread, error) { + return &entities.MessageThread{ID: threadID}, nil +} + +func (repository *listenerMessageThreadRepository) UpdateAfterDeletedMessage(context.Context, repositories.MessageThreadDeletedUpdate) error { + return nil +} + +func (repository *listenerMessageThreadRepository) LoadByOwnerContact(context.Context, entities.UserID, string, string) (*entities.MessageThread, error) { + return &entities.MessageThread{ID: uuid.New()}, nil +} + +func (repository *listenerMessageThreadRepository) Load(context.Context, entities.UserID, uuid.UUID) (*entities.MessageThread, error) { + return &entities.MessageThread{}, nil +} + +func (repository *listenerMessageThreadRepository) Index(context.Context, entities.UserID, string, bool, repositories.IndexParams) (*[]entities.MessageThread, error) { + threads := []entities.MessageThread{} + return &threads, nil +} + +func (repository *listenerMessageThreadRepository) Delete(context.Context, entities.UserID, uuid.UUID) error { + return nil +} + +func (repository *listenerMessageThreadRepository) DeleteAllForUser(context.Context, entities.UserID) error { + return nil +} diff --git a/api/pkg/listeners/websocket_listener.go b/api/pkg/listeners/websocket_listener.go index 3108ed671..8aa9054f0 100644 --- a/api/pkg/listeners/websocket_listener.go +++ b/api/pkg/listeners/websocket_listener.go @@ -36,9 +36,25 @@ func NewWebsocketListener( events.EventTypeMessagePhoneSent: l.onMessagePhoneSent, events.EventTypeMessageSendFailed: l.onMessagePhoneFailed, events.EventTypeMessagePhoneReceived: l.onMessagePhoneReceived, + events.MessageCallMissed: l.onMessageCallMissed, } } +func (listener *WebsocketListener) onMessageCallMissed(ctx context.Context, event cloudevents.Event) error { + ctx, span, _ := listener.tracer.StartWithLogger(ctx, listener.logger) + defer span.End() + + var payload events.MessageCallMissedPayload + if err := event.DataAs(&payload); err != nil { + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot decode [%s] into [%T]", event.Data(), payload)) + } + + if err := listener.client.Trigger(payload.UserID.String(), event.Type(), event.ID()); err != nil { + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot trigger websocket [%s] event with ID [%s] for user with ID [%s]", event.Type(), event.ID(), payload.UserID)) + } + return nil +} + // onMessagePhoneSent handles the events.EventTypeMessagePhoneSent event func (listener *WebsocketListener) onMessagePhoneSent(ctx context.Context, event cloudevents.Event) error { ctx, span, _ := listener.tracer.StartWithLogger(ctx, listener.logger) diff --git a/api/pkg/listeners/websocket_listener_test.go b/api/pkg/listeners/websocket_listener_test.go new file mode 100644 index 000000000..fdaf1c6d5 --- /dev/null +++ b/api/pkg/listeners/websocket_listener_test.go @@ -0,0 +1,18 @@ +package listeners + +import ( + "testing" + + "github.com/NdoleStudio/httpsms/pkg/events" + "github.com/NdoleStudio/httpsms/pkg/telemetry" + "github.com/pusher/pusher-http-go/v5" + "github.com/stretchr/testify/assert" +) + +func TestWebsocketListenerRegistersMissedCalls(t *testing.T) { + logger := &noopListenerLogger{} + tracer := telemetry.NewOtelLogger("test", logger) + _, routes := NewWebsocketListener(logger, tracer, &pusher.Client{}) + + assert.Contains(t, routes, events.MessageCallMissed) +} diff --git a/api/pkg/repositories/gorm_message_thread_repository.go b/api/pkg/repositories/gorm_message_thread_repository.go index 7bc07f7b3..071f5cc79 100644 --- a/api/pkg/repositories/gorm_message_thread_repository.go +++ b/api/pkg/repositories/gorm_message_thread_repository.go @@ -35,6 +35,48 @@ func NewGormMessageThreadRepository( } } +func messageThreadActivityUpdates(params MessageThreadActivityUpdate) map[string]any { + updates := map[string]any{ + "order_timestamp": params.Timestamp, + "last_message_id": params.MessageID, + "last_message_content": params.Content, + "status": params.Status, + } + if params.Unarchive { + updates["is_archived"] = false + } + if params.MarkAsUnread { + updates["is_read"] = gorm.Expr( + "CASE WHEN last_read_at < ? THEN ? ELSE is_read END", + params.EventTimestamp, + false, + ) + } + return updates +} + +func messageThreadDeletedUpdates(params MessageThreadDeletedUpdate) map[string]any { + return map[string]any{ + "last_message_id": params.LastMessageID, + "last_message_content": params.LastMessageContent, + "status": params.LastMessageStatus, + } +} + +func messageThreadStatusUpdates(params MessageThreadStatusUpdate) map[string]any { + updates := make(map[string]any) + if params.IsArchived != nil { + updates["is_archived"] = *params.IsArchived + } + if params.IsRead != nil { + updates["is_read"] = *params.IsRead + if *params.IsRead { + updates["last_read_at"] = params.ReadAt + } + } + return updates +} + func (repository *gormMessageThreadRepository) DeleteAllForUser(ctx context.Context, userID entities.UserID) error { ctx, span := repository.tracer.Start(ctx) defer span.End() @@ -60,20 +102,17 @@ func (repository *gormMessageThreadRepository) Delete(ctx context.Context, userI } // UpdateAfterDeletedMessage updates a thread after the original message has been deleted -func (repository *gormMessageThreadRepository) UpdateAfterDeletedMessage(ctx context.Context, userID entities.UserID, messageID uuid.UUID) error { +func (repository *gormMessageThreadRepository) UpdateAfterDeletedMessage(ctx context.Context, params MessageThreadDeletedUpdate) error { ctx, span := repository.tracer.Start(ctx) defer span.End() - err := repository.db.WithContext(ctx).Model(&entities.MessageThread{}). - Where("user_id = ?", userID). - Where("last_message_id = ?", messageID). - Updates(map[string]any{ - "last_message_id": nil, - "last_message_content": nil, - "status": entities.MessageStatusDeleted, - }).Error - if err != nil { - return repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot update thread after message is deleted with userID [%s] and messageID [%s]", userID, messageID)) + result := repository.db.WithContext(ctx). + Model(&entities.MessageThread{}). + Where("user_id = ?", params.UserID). + Where("id = ?", params.MessageThreadID). + Updates(messageThreadDeletedUpdates(params)) + if result.Error != nil { + return repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(result.Error, "cannot update deleted-message metadata for thread [%s]", params.MessageThreadID)) } return nil @@ -84,25 +123,77 @@ func (repository *gormMessageThreadRepository) Store(ctx context.Context, thread ctx, span := repository.tracer.Start(ctx) defer span.End() - if err := repository.db.WithContext(ctx).Clauses(clause.OnConflict{DoNothing: true}).Create(thread).Error; err != nil { + isRead := thread.IsRead + err := repository.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { + result := tx.Clauses(clause.OnConflict{DoNothing: true}).Create(thread) + thread.IsRead = isRead + if result.Error != nil { + return result.Error + } + if result.RowsAffected == 0 || isRead { + return nil + } + + return tx.Model(&entities.MessageThread{}). + Where("user_id = ?", thread.UserID). + Where("id = ?", thread.ID). + UpdateColumn("is_read", false). + Error + }) + if err != nil { return repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot save message thread with ID [%s]", thread.ID)) } return nil } -// Update a new entities.MessageThread -func (repository *gormMessageThreadRepository) Update(ctx context.Context, thread *entities.MessageThread) error { +// UpdateActivity persists the last-message activity fields for a thread +func (repository *gormMessageThreadRepository) UpdateActivity(ctx context.Context, params MessageThreadActivityUpdate) error { ctx, span := repository.tracer.Start(ctx) defer span.End() - if err := repository.db.WithContext(ctx).Save(thread).Error; err != nil { - return repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot update message thread thread with ID [%s]", thread.ID)) + result := repository.db.WithContext(ctx). + Model(&entities.MessageThread{}). + Where("user_id = ?", params.UserID). + Where("id = ?", params.MessageThreadID). + Updates(messageThreadActivityUpdates(params)) + if result.Error != nil { + return repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(result.Error, "cannot update message activity for thread [%s]", params.MessageThreadID)) + } + if result.RowsAffected == 0 { + return repository.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCode(gorm.ErrRecordNotFound, ErrCodeNotFound, "thread with id [%s] not found", params.MessageThreadID)) } return nil } +// UpdateStatus persists archive/read status fields for a thread +func (repository *gormMessageThreadRepository) UpdateStatus( + ctx context.Context, + userID entities.UserID, + messageThreadID uuid.UUID, + params MessageThreadStatusUpdate, +) (*entities.MessageThread, error) { + ctx, span := repository.tracer.Start(ctx) + defer span.End() + + thread := new(entities.MessageThread) + result := repository.db.WithContext(ctx). + Model(thread). + Clauses(clause.Returning{}). + Where("user_id = ?", userID). + Where("id = ?", messageThreadID). + Updates(messageThreadStatusUpdates(params)) + if result.Error != nil { + return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(result.Error, "cannot update status for thread [%s] and user [%s]", messageThreadID, userID)) + } + if result.RowsAffected == 0 { + return nil, repository.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCode(gorm.ErrRecordNotFound, ErrCodeNotFound, "thread with id [%s] not found for user with ID [%s]", messageThreadID, userID)) + } + + return thread, nil +} + // LoadByOwnerContact a thread between 2 users func (repository *gormMessageThreadRepository) LoadByOwnerContact(ctx context.Context, userID entities.UserID, owner string, contact string) (*entities.MessageThread, error) { ctx, span := repository.tracer.Start(ctx) diff --git a/api/pkg/repositories/gorm_message_thread_repository_test.go b/api/pkg/repositories/gorm_message_thread_repository_test.go new file mode 100644 index 000000000..31dc43ed5 --- /dev/null +++ b/api/pkg/repositories/gorm_message_thread_repository_test.go @@ -0,0 +1,204 @@ +package repositories + +import ( + "context" + "database/sql" + "database/sql/driver" + "errors" + "strings" + "testing" + "time" + + "github.com/NdoleStudio/httpsms/pkg/entities" + "github.com/NdoleStudio/httpsms/pkg/telemetry" + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.opentelemetry.io/otel/trace" + "gorm.io/driver/postgres" + "gorm.io/gorm" +) + +type messageThreadTestStatement struct { + query string + args []any +} + +type messageThreadTestConnPool struct { + statements []messageThreadTestStatement +} + +func (messageThreadTestConnPool) PrepareContext(context.Context, string) (*sql.Stmt, error) { + return nil, errors.New("unexpected PrepareContext") +} + +func (pool *messageThreadTestConnPool) ExecContext(_ context.Context, query string, args ...any) (sql.Result, error) { + pool.statements = append(pool.statements, messageThreadTestStatement{ + query: query, + args: append([]any(nil), args...), + }) + return driver.RowsAffected(1), nil +} + +func (messageThreadTestConnPool) QueryContext(context.Context, string, ...any) (*sql.Rows, error) { + return nil, errors.New("unexpected QueryContext") +} + +func (messageThreadTestConnPool) QueryRowContext(context.Context, string, ...any) *sql.Row { + return &sql.Row{} +} + +func (pool *messageThreadTestConnPool) BeginTx(context.Context, *sql.TxOptions) (gorm.ConnPool, error) { + return pool, nil +} + +func (*messageThreadTestConnPool) Commit() error { + return nil +} + +func (*messageThreadTestConnPool) Rollback() error { + return nil +} + +type messageThreadTestLogger struct{} + +func (logger *messageThreadTestLogger) Error(error) {} +func (logger *messageThreadTestLogger) WithService(string) telemetry.Logger { return logger } + +func (logger *messageThreadTestLogger) WithString(string, string) telemetry.Logger { return logger } + +func (logger *messageThreadTestLogger) WithSpan(trace.SpanContext) telemetry.Logger { return logger } +func (logger *messageThreadTestLogger) Trace(string) {} +func (logger *messageThreadTestLogger) Info(string) {} +func (logger *messageThreadTestLogger) Warn(error) {} +func (logger *messageThreadTestLogger) Debug(string) {} +func (logger *messageThreadTestLogger) Fatal(error) {} +func (logger *messageThreadTestLogger) Printf(string, ...interface{}) {} + +func TestMessageThreadStorePreservesExplicitUnreadState(t *testing.T) { + pool := &messageThreadTestConnPool{} + db, err := gorm.Open( + postgres.New(postgres.Config{ + Conn: pool, + WithoutReturning: true, + }), + &gorm.Config{DisableAutomaticPing: true}, + ) + require.NoError(t, err) + + logger := &messageThreadTestLogger{} + repository := NewGormMessageThreadRepository(logger, telemetry.NewOtelLogger("test", logger), db) + thread := &entities.MessageThread{ + ID: uuid.New(), + IsRead: false, + } + + require.NoError(t, repository.Store(context.Background(), thread)) + assert.False(t, thread.IsRead) + + require.NotEmpty(t, pool.statements) + update := pool.statements[len(pool.statements)-1] + assert.True(t, strings.HasPrefix(update.query, `UPDATE "message_threads"`)) + assert.Contains(t, update.query, `"is_read"=$1`) + assert.Contains(t, update.args, false) +} + +func TestMessageThreadActivityUpdatesOwnOnlyMessageColumns(t *testing.T) { + messageID := uuid.New() + updates := messageThreadActivityUpdates(MessageThreadActivityUpdate{ + Timestamp: time.Date(2026, 7, 18, 7, 0, 0, 0, time.UTC), + MessageID: messageID, + Content: "hello", + Status: entities.MessageStatusReceived, + }) + + assert.Equal(t, map[string]any{ + "order_timestamp": time.Date(2026, 7, 18, 7, 0, 0, 0, time.UTC), + "last_message_id": messageID, + "last_message_content": "hello", + "status": entities.MessageStatus(entities.MessageStatusReceived), + }, updates) + assert.NotContains(t, updates, "is_read") + assert.NotContains(t, updates, "is_archived") + assert.NotContains(t, updates, "last_read_at") +} + +func TestUpdateActivityMarksUnreadWithOneQuery(t *testing.T) { + pool := &messageThreadTestConnPool{} + db, err := gorm.Open( + postgres.New(postgres.Config{ + Conn: pool, + WithoutReturning: true, + }), + &gorm.Config{DisableAutomaticPing: true}, + ) + require.NoError(t, err) + + logger := &messageThreadTestLogger{} + repository := NewGormMessageThreadRepository(logger, telemetry.NewOtelLogger("test", logger), db) + + err = repository.UpdateActivity(context.Background(), MessageThreadActivityUpdate{ + MessageThreadID: uuid.New(), + UserID: entities.UserID("user-id"), + Timestamp: time.Date(2026, 7, 19, 10, 0, 0, 0, time.UTC), + MessageID: uuid.New(), + Content: "hello", + Status: entities.MessageStatusReceived, + MarkAsUnread: true, + EventTimestamp: time.Date(2026, 7, 19, 10, 0, 1, 0, time.UTC), + }) + + require.NoError(t, err) + var updates []messageThreadTestStatement + for _, statement := range pool.statements { + if strings.HasPrefix(statement.query, `UPDATE "message_threads"`) { + updates = append(updates, statement) + } + } + require.Len(t, updates, 1) + assert.Contains(t, updates[0].query, `"is_read"=CASE WHEN last_read_at <`) +} + +func TestMessageThreadDeletedUpdatesPreserveStatusType(t *testing.T) { + messageID := uuid.New() + content := "previous message" + updates := messageThreadDeletedUpdates(MessageThreadDeletedUpdate{ + LastMessageID: &messageID, + LastMessageContent: &content, + LastMessageStatus: entities.MessageStatusDelivered, + }) + + assert.Equal(t, map[string]any{ + "last_message_id": &messageID, + "last_message_content": &content, + "status": entities.MessageStatus(entities.MessageStatusDelivered), + }, updates) +} + +func TestMessageThreadStatusUpdatesReadOnly(t *testing.T) { + isRead := true + readAt := time.Date(2026, 7, 18, 7, 1, 0, 0, time.UTC) + + updates := messageThreadStatusUpdates(MessageThreadStatusUpdate{ + IsRead: &isRead, + ReadAt: readAt, + }) + + assert.Equal(t, map[string]any{ + "is_read": true, + "last_read_at": readAt, + }, updates) + assert.NotContains(t, updates, "is_archived") +} + +func TestMessageThreadStatusUpdatesArchiveOnly(t *testing.T) { + isArchived := true + + updates := messageThreadStatusUpdates(MessageThreadStatusUpdate{ + IsArchived: &isArchived, + }) + + assert.Equal(t, map[string]any{"is_archived": true}, updates) + assert.NotContains(t, updates, "is_read") + assert.NotContains(t, updates, "last_read_at") +} diff --git a/api/pkg/repositories/message_thread_repository.go b/api/pkg/repositories/message_thread_repository.go index 542fd519b..e60931419 100644 --- a/api/pkg/repositories/message_thread_repository.go +++ b/api/pkg/repositories/message_thread_repository.go @@ -2,19 +2,50 @@ package repositories import ( "context" + "time" "github.com/google/uuid" "github.com/NdoleStudio/httpsms/pkg/entities" ) +type MessageThreadActivityUpdate struct { + MessageThreadID uuid.UUID + UserID entities.UserID + // Timestamp controls thread activity ordering; EventTimestamp is the server-side unread watermark. + Timestamp time.Time + MessageID uuid.UUID + Content string + Status entities.MessageStatus + MarkAsUnread bool + EventTimestamp time.Time + Unarchive bool +} + +type MessageThreadStatusUpdate struct { + IsArchived *bool + IsRead *bool + ReadAt time.Time +} + +type MessageThreadDeletedUpdate struct { + MessageThreadID uuid.UUID + UserID entities.UserID + LastMessageID *uuid.UUID + LastMessageContent *string + LastMessageStatus entities.MessageStatus +} + // MessageThreadRepository loads and persists an entities.MessageThread type MessageThreadRepository interface { // Store a new entities.MessageThread Store(ctx context.Context, thread *entities.MessageThread) error - // Update a new entities.MessageThread - Update(ctx context.Context, thread *entities.MessageThread) error + // UpdateActivity persists the last-message activity fields for a thread + UpdateActivity(ctx context.Context, params MessageThreadActivityUpdate) error + + // UpdateStatus persists archive/read status fields for a thread + UpdateStatus(ctx context.Context, userID entities.UserID, messageThreadID uuid.UUID, params MessageThreadStatusUpdate) (*entities.MessageThread, error) // LoadByOwnerContact fetches a thread between owner and contact LoadByOwnerContact(ctx context.Context, userID entities.UserID, owner string, contact string) (*entities.MessageThread, error) @@ -26,7 +57,7 @@ type MessageThreadRepository interface { Index(ctx context.Context, userID entities.UserID, owner string, archived bool, params IndexParams) (*[]entities.MessageThread, error) // UpdateAfterDeletedMessage updates a thread after the original message has been deleted - UpdateAfterDeletedMessage(ctx context.Context, userID entities.UserID, messageID uuid.UUID) error + UpdateAfterDeletedMessage(ctx context.Context, params MessageThreadDeletedUpdate) error // Delete an entities.MessageThread by ID Delete(ctx context.Context, userID entities.UserID, messageThreadID uuid.UUID) error diff --git a/api/pkg/requests/message_thread_update_request.go b/api/pkg/requests/message_thread_update_request.go index d64d75c59..3309fc91d 100644 --- a/api/pkg/requests/message_thread_update_request.go +++ b/api/pkg/requests/message_thread_update_request.go @@ -10,7 +10,8 @@ import ( // MessageThreadUpdate is the payload for updating a message thread type MessageThreadUpdate struct { request - IsArchived bool `json:"is_archived" example:"true"` + IsArchived *bool `json:"is_archived,omitempty" example:"true"` + IsRead *bool `json:"is_read,omitempty" example:"true"` MessageThreadID string `json:"messageThreadID" swaggerignore:"true"` // used internally for validation } @@ -21,5 +22,6 @@ func (input *MessageThreadUpdate) ToUpdateParams(userID entities.UserID) service UserID: userID, MessageThreadID: uuid.MustParse(input.MessageThreadID), IsArchived: input.IsArchived, + IsRead: input.IsRead, } } diff --git a/api/pkg/requests/message_thread_update_request_test.go b/api/pkg/requests/message_thread_update_request_test.go new file mode 100644 index 000000000..9f9579fda --- /dev/null +++ b/api/pkg/requests/message_thread_update_request_test.go @@ -0,0 +1,25 @@ +package requests + +import ( + "testing" + + "github.com/NdoleStudio/httpsms/pkg/entities" + "github.com/google/uuid" + "github.com/stretchr/testify/assert" +) + +func TestMessageThreadUpdateToUpdateParamsPreservesOptionalFields(t *testing.T) { + threadID := uuid.New() + isRead := true + input := MessageThreadUpdate{ + MessageThreadID: threadID.String(), + IsRead: &isRead, + } + + params := input.ToUpdateParams(entities.UserID("user-id")) + + assert.Equal(t, threadID, params.MessageThreadID) + assert.Equal(t, entities.UserID("user-id"), params.UserID) + assert.Nil(t, params.IsArchived) + assert.Same(t, &isRead, params.IsRead) +} diff --git a/api/pkg/responses/message_thead_responses.go b/api/pkg/responses/message_thead_responses.go index 917706a47..e47fec287 100644 --- a/api/pkg/responses/message_thead_responses.go +++ b/api/pkg/responses/message_thead_responses.go @@ -7,3 +7,9 @@ type MessageThreadsResponse struct { response Data []entities.MessageThread `json:"data"` } + +// MessageThreadResponse is the payload containing entities.MessageThread +type MessageThreadResponse struct { + response + Data entities.MessageThread `json:"data"` +} diff --git a/api/pkg/services/message_thread_service.go b/api/pkg/services/message_thread_service.go index c77458cbf..e3dc882c7 100644 --- a/api/pkg/services/message_thread_service.go +++ b/api/pkg/services/message_thread_service.go @@ -50,7 +50,10 @@ type MessageThreadUpdateParams struct { Content string UserID entities.UserID MessageID uuid.UUID - Timestamp time.Time + // Timestamp controls thread activity ordering; EventTimestamp is the server-side unread watermark. + Timestamp time.Time + MarkAsUnread bool + EventTimestamp time.Time } // shouldCheckUnarchive reports whether a thread update is a new inbound message @@ -101,27 +104,39 @@ func (service *MessageThreadService) UpdateThread(ctx context.Context, params Me return nil } + activity := repositories.MessageThreadActivityUpdate{ + MessageThreadID: thread.ID, + UserID: params.UserID, + Timestamp: params.Timestamp, + MessageID: params.MessageID, + Content: params.Content, + Status: params.Status, + MarkAsUnread: params.MarkAsUnread, + EventTimestamp: params.EventTimestamp, + } + if service.shouldCheckUnarchive(thread, params) { phone, phoneErr := service.phoneRepository.Load(ctx, params.UserID, params.Owner) if phoneErr != nil { ctxLogger.Warn(stacktrace.Propagate(phoneErr, "cannot load phone [%s] for user [%s] to resolve UnarchiveThread; leaving thread [%s] archived", params.Owner, params.UserID, thread.ID)) } else if phone.UnarchiveThread { - thread.UpdateArchive(false) + activity.Unarchive = true ctxLogger.Info(fmt.Sprintf("unarchiving thread [%s] after inbound message [%s]", thread.ID, params.MessageID)) } } - if err = service.repository.Update(ctx, thread.Update(params.Timestamp, params.MessageID, params.Content, params.Status)); err != nil { - return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot update message thread with id [%s] after adding message [%s]", thread.ID, params.MessageID)) + if err = service.repository.UpdateActivity(ctx, activity); err != nil { + return service.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCode(err, stacktrace.GetCode(err), "cannot update message thread with id [%s] after adding message [%s]", thread.ID, params.MessageID)) } - ctxLogger.Info(fmt.Sprintf("thread with id [%s] updated with last message [%s] and status [%s]", thread.ID, thread.LastMessageID, thread.Status)) + ctxLogger.Info(fmt.Sprintf("thread with id [%s] updated with last message [%s] and status [%s]", thread.ID, params.MessageID, params.Status)) return nil } // MessageThreadStatusParams are parameters for updating a thread status type MessageThreadStatusParams struct { - IsArchived bool + IsArchived *bool + IsRead *bool UserID entities.UserID MessageThreadID uuid.UUID } @@ -131,18 +146,16 @@ func (service *MessageThreadService) UpdateStatus(ctx context.Context, params Me ctx, span := service.tracer.Start(ctx) defer span.End() - ctxLogger := service.tracer.CtxLogger(service.logger, span) - - thread, err := service.repository.Load(ctx, params.UserID, params.MessageThreadID) - if err != nil { - return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot find thread with id [%s]", params.MessageThreadID)) + update := repositories.MessageThreadStatusUpdate{ + IsArchived: params.IsArchived, + IsRead: params.IsRead, + ReadAt: time.Now().UTC(), } - - if err = service.repository.Update(ctx, thread.UpdateArchive(params.IsArchived)); err != nil { - return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot update message thread with id [%s] with archive status [%t]", thread.ID, params.IsArchived)) + thread, err := service.repository.UpdateStatus(ctx, params.UserID, params.MessageThreadID, update) + if err != nil { + return nil, service.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCode(err, stacktrace.GetCode(err), "cannot update message thread with ID [%s] for user [%s]", params.MessageThreadID, params.UserID)) } - ctxLogger.Info(fmt.Sprintf("thread with id [%s] updated with archive status [%t]", thread.ID, thread.IsArchived)) return thread, nil } @@ -172,12 +185,13 @@ func (service *MessageThreadService) UpdateAfterDeletedMessage(ctx context.Conte return nil } - thread.LastMessageContent = payload.PreviousMessageContent - thread.LastMessageID = payload.PreviousMessageID - thread.Status = *payload.PreviousMessageStatus - thread.UpdatedAt = time.Now().UTC() - - if err = service.repository.Update(ctx, thread); err != nil { + if err = service.repository.UpdateAfterDeletedMessage(ctx, repositories.MessageThreadDeletedUpdate{ + MessageThreadID: thread.ID, + UserID: thread.UserID, + LastMessageID: payload.PreviousMessageID, + LastMessageContent: payload.PreviousMessageContent, + LastMessageStatus: *payload.PreviousMessageStatus, + }); err != nil { return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot update thread with ID [%s] for user with ID [%s]", thread.ID, thread.UserID)) } @@ -191,18 +205,21 @@ func (service *MessageThreadService) createThread(ctx context.Context, params Me ctxLogger := service.tracer.CtxLogger(service.logger, span) + now := time.Now().UTC() thread := &entities.MessageThread{ ID: uuid.New(), Owner: params.Owner, Contact: params.Contact, UserID: params.UserID, IsArchived: false, + IsRead: !params.MarkAsUnread, + LastReadAt: now, Color: service.getColor(), LastMessageContent: ¶ms.Content, Status: params.Status, LastMessageID: ¶ms.MessageID, - CreatedAt: time.Now().UTC(), - UpdatedAt: time.Now().UTC(), + CreatedAt: now, + UpdatedAt: now, OrderTimestamp: params.Timestamp, } diff --git a/api/pkg/services/message_thread_service_test.go b/api/pkg/services/message_thread_service_test.go index 7e48ab433..fc1e84313 100644 --- a/api/pkg/services/message_thread_service_test.go +++ b/api/pkg/services/message_thread_service_test.go @@ -1,12 +1,226 @@ package services import ( + "context" "testing" + "time" "github.com/NdoleStudio/httpsms/pkg/entities" + "github.com/NdoleStudio/httpsms/pkg/repositories" + "github.com/NdoleStudio/httpsms/pkg/telemetry" + "github.com/google/uuid" + "github.com/palantir/stacktrace" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "gorm.io/gorm" ) +type messageThreadRepositoryStub struct { + loadByOwnerContact func(context.Context, entities.UserID, string, string) (*entities.MessageThread, error) + load func(context.Context, entities.UserID, uuid.UUID) (*entities.MessageThread, error) + store func(context.Context, *entities.MessageThread) error + updateActivity func(context.Context, repositories.MessageThreadActivityUpdate) error + updateStatus func(context.Context, entities.UserID, uuid.UUID, repositories.MessageThreadStatusUpdate) (*entities.MessageThread, error) +} + +func (stub *messageThreadRepositoryStub) Store(ctx context.Context, thread *entities.MessageThread) error { + if stub.store != nil { + return stub.store(ctx, thread) + } + return nil +} + +func (stub *messageThreadRepositoryStub) UpdateActivity(ctx context.Context, params repositories.MessageThreadActivityUpdate) error { + if stub.updateActivity != nil { + return stub.updateActivity(ctx, params) + } + return nil +} + +func (stub *messageThreadRepositoryStub) UpdateStatus(ctx context.Context, userID entities.UserID, threadID uuid.UUID, params repositories.MessageThreadStatusUpdate) (*entities.MessageThread, error) { + if stub.updateStatus != nil { + return stub.updateStatus(ctx, userID, threadID, params) + } + return &entities.MessageThread{ID: threadID}, nil +} + +func (stub *messageThreadRepositoryStub) UpdateAfterDeletedMessage(context.Context, repositories.MessageThreadDeletedUpdate) error { + return nil +} + +func (stub *messageThreadRepositoryStub) LoadByOwnerContact(ctx context.Context, userID entities.UserID, owner string, contact string) (*entities.MessageThread, error) { + return stub.loadByOwnerContact(ctx, userID, owner, contact) +} + +func (stub *messageThreadRepositoryStub) Load(ctx context.Context, userID entities.UserID, id uuid.UUID) (*entities.MessageThread, error) { + return stub.load(ctx, userID, id) +} + +func (stub *messageThreadRepositoryStub) Index(context.Context, entities.UserID, string, bool, repositories.IndexParams) (*[]entities.MessageThread, error) { + threads := []entities.MessageThread{} + return &threads, nil +} + +func (stub *messageThreadRepositoryStub) Delete(context.Context, entities.UserID, uuid.UUID) error { + return nil +} + +func (stub *messageThreadRepositoryStub) DeleteAllForUser(context.Context, entities.UserID) error { + return nil +} + +func newMessageThreadServiceForTest(repository repositories.MessageThreadRepository) *MessageThreadService { + logger := &noopLogger{} + tracer := telemetry.NewOtelLogger("test", logger) + return NewMessageThreadService(logger, tracer, repository, nil, nil) +} + +func TestUpdateThreadPassesUnreadWatermarkForInboundActivity(t *testing.T) { + threadID := uuid.New() + eventTimestamp := time.Date(2026, 7, 18, 7, 0, 0, 0, time.UTC) + var captured repositories.MessageThreadActivityUpdate + repository := &messageThreadRepositoryStub{ + loadByOwnerContact: func(context.Context, entities.UserID, string, string) (*entities.MessageThread, error) { + return &entities.MessageThread{ID: threadID}, nil + }, + updateActivity: func(_ context.Context, params repositories.MessageThreadActivityUpdate) error { + captured = params + return nil + }, + } + + service := newMessageThreadServiceForTest(repository) + err := service.UpdateThread(context.Background(), MessageThreadUpdateParams{ + UserID: entities.UserID("user-id"), + Owner: "+18005550199", + Contact: "+18005550100", + MessageID: uuid.New(), + Content: "hello", + Status: entities.MessageStatusReceived, + Timestamp: eventTimestamp, + MarkAsUnread: true, + EventTimestamp: eventTimestamp, + }) + + require.NoError(t, err) + assert.True(t, captured.MarkAsUnread) + assert.Equal(t, eventTimestamp, captured.EventTimestamp) +} + +func TestUpdateThreadPreservesReadStateForOutboundActivity(t *testing.T) { + var captured repositories.MessageThreadActivityUpdate + repository := &messageThreadRepositoryStub{ + loadByOwnerContact: func(context.Context, entities.UserID, string, string) (*entities.MessageThread, error) { + return &entities.MessageThread{ID: uuid.New(), IsRead: false}, nil + }, + updateActivity: func(_ context.Context, params repositories.MessageThreadActivityUpdate) error { + captured = params + return nil + }, + } + + service := newMessageThreadServiceForTest(repository) + err := service.UpdateThread(context.Background(), MessageThreadUpdateParams{ + UserID: entities.UserID("user-id"), + Owner: "+18005550199", + Contact: "+18005550100", + MessageID: uuid.New(), + Content: "outbound", + Status: entities.MessageStatusSent, + Timestamp: time.Now().UTC(), + }) + + require.NoError(t, err) + assert.False(t, captured.MarkAsUnread) +} + +func TestCreateThreadSetsReadStateFromActivityDirection(t *testing.T) { + tests := []struct { + name string + marksUnread bool + wantRead bool + }{ + {name: "inbound", marksUnread: true, wantRead: false}, + {name: "outbound", marksUnread: false, wantRead: true}, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + var stored *entities.MessageThread + repository := &messageThreadRepositoryStub{ + loadByOwnerContact: func(context.Context, entities.UserID, string, string) (*entities.MessageThread, error) { + return nil, stacktrace.PropagateWithCode(gorm.ErrRecordNotFound, repositories.ErrCodeNotFound, "not found") + }, + store: func(_ context.Context, thread *entities.MessageThread) error { + stored = thread + return nil + }, + } + + service := newMessageThreadServiceForTest(repository) + err := service.UpdateThread(context.Background(), MessageThreadUpdateParams{ + UserID: entities.UserID("user-id"), + Owner: "+18005550199", + Contact: "+18005550100", + MessageID: uuid.New(), + Content: "hello", + Status: entities.MessageStatusReceived, + Timestamp: time.Now().UTC(), + MarkAsUnread: test.marksUnread, + }) + + require.NoError(t, err) + require.NotNil(t, stored) + assert.Equal(t, test.wantRead, stored.IsRead) + assert.False(t, stored.LastReadAt.IsZero()) + }) + } +} + +func TestUpdateStatusChangesOnlyRequestedState(t *testing.T) { + threadID := uuid.New() + isRead := false + var captured repositories.MessageThreadStatusUpdate + repository := &messageThreadRepositoryStub{ + updateStatus: func(_ context.Context, _ entities.UserID, _ uuid.UUID, params repositories.MessageThreadStatusUpdate) (*entities.MessageThread, error) { + captured = params + return &entities.MessageThread{ID: threadID, IsArchived: true, IsRead: false}, nil + }, + } + + service := newMessageThreadServiceForTest(repository) + thread, err := service.UpdateStatus(context.Background(), MessageThreadStatusParams{ + UserID: entities.UserID("user-id"), + MessageThreadID: threadID, + IsRead: &isRead, + }) + + require.NoError(t, err) + assert.Nil(t, captured.IsArchived) + assert.Same(t, &isRead, captured.IsRead) + assert.False(t, captured.ReadAt.IsZero()) + assert.True(t, thread.IsArchived) + assert.False(t, thread.IsRead) +} + +func TestUpdateStatusPreservesNotFoundCode(t *testing.T) { + repository := &messageThreadRepositoryStub{ + updateStatus: func(context.Context, entities.UserID, uuid.UUID, repositories.MessageThreadStatusUpdate) (*entities.MessageThread, error) { + return nil, stacktrace.PropagateWithCode(gorm.ErrRecordNotFound, repositories.ErrCodeNotFound, "not found") + }, + } + + service := newMessageThreadServiceForTest(repository) + isRead := true + _, err := service.UpdateStatus(context.Background(), MessageThreadStatusParams{ + UserID: entities.UserID("user-id"), + MessageThreadID: uuid.New(), + IsRead: &isRead, + }) + + assert.Equal(t, repositories.ErrCodeNotFound, stacktrace.GetCode(err)) +} + func TestShouldCheckUnarchive(t *testing.T) { service := &MessageThreadService{} diff --git a/api/pkg/validators/message_thread_handler_validator.go b/api/pkg/validators/message_thread_handler_validator.go index a4e5cd647..72a64194f 100644 --- a/api/pkg/validators/message_thread_handler_validator.go +++ b/api/pkg/validators/message_thread_handler_validator.go @@ -72,5 +72,13 @@ func (validator *MessageThreadHandlerValidator) ValidateUpdate(_ context.Context }, }) - return v.ValidateStruct() + errors := v.ValidateStruct() + if request.IsArchived == nil && request.IsRead == nil { + if errors == nil { + errors = url.Values{} + } + errors.Add("payload", "at least one of is_archived or is_read is required") + } + + return errors } diff --git a/api/pkg/validators/message_thread_handler_validator_test.go b/api/pkg/validators/message_thread_handler_validator_test.go new file mode 100644 index 000000000..e5ec7c1b7 --- /dev/null +++ b/api/pkg/validators/message_thread_handler_validator_test.go @@ -0,0 +1,34 @@ +package validators + +import ( + "context" + "testing" + + "github.com/NdoleStudio/httpsms/pkg/requests" + "github.com/google/uuid" + "github.com/stretchr/testify/assert" +) + +func TestValidateUpdateRequiresAtLeastOneStatusField(t *testing.T) { + validator := &MessageThreadHandlerValidator{} + request := requests.MessageThreadUpdate{ + MessageThreadID: uuid.NewString(), + } + + errors := validator.ValidateUpdate(context.Background(), request) + + assert.NotEmpty(t, errors.Get("payload")) +} + +func TestValidateUpdateAcceptsReadOnlyUpdate(t *testing.T) { + validator := &MessageThreadHandlerValidator{} + isRead := true + request := requests.MessageThreadUpdate{ + MessageThreadID: uuid.NewString(), + IsRead: &isRead, + } + + errors := validator.ValidateUpdate(context.Background(), request) + + assert.Empty(t, errors) +} diff --git a/docs/superpowers/plans/2026-07-18-read-receipts.md b/docs/superpowers/plans/2026-07-18-read-receipts.md new file mode 100644 index 000000000..1887a1511 --- /dev/null +++ b/docs/superpowers/plans/2026-07-18-read-receipts.md @@ -0,0 +1,1760 @@ +# Message Thread Read Receipts Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Persist message-thread read state, mark inbound SMS and missed calls unread, automatically mark opened threads read, and highlight unread threads in the web UI. + +**Architecture:** Store `is_read` plus an internal `last_read_at` watermark on each message thread. Replace full-row thread saves with field-scoped GORM updates so concurrent event listeners cannot overwrite a newer read action; inbound updates compare CloudEvent creation time against the stored watermark. Extend the existing thread update endpoint with optional archive/read fields, then have the Nuxt store call it automatically when the thread page opens or refreshes after inbound realtime events. + +**Tech Stack:** Go 1.25, Fiber v3, GORM/PostgreSQL, CloudEvents, Pusher, Nuxt 4 SPA, Vue 3, Pinia, Vuetify 4, TypeScript. + +## Global Constraints + +- Work only in `C:\Users\achoa\Work\NdoleStudio\httpsms-read-receipts` on branch `feat/read-receipts`, based on `origin/main`. +- Existing message threads must migrate as read. +- Only incoming SMS and missed-call activity marks a thread unread. +- Outbound messages and delivery/status events preserve the existing read state. +- Opening a thread marks it read automatically; inbound activity while it is open must leave it read. +- Do not add a new endpoint or a manual read/unread control. +- Use the existing `PUT /v1/message-threads/{messageThreadID}` endpoint with optional `is_archived` and `is_read`. +- Use GORM query builders with `WithContext(ctx)`; do not use raw SQL. +- Wrap API errors with `stacktrace.Propagate`/`PropagateWithCode`. +- Web code uses single quotes, no semicolons, and 2-space indentation. +- Every commit must end with: + +```text +Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> +Copilot-Session: bf00a0ac-e11f-4015-b295-3cdd9b491229 +``` + +- Add end-to-end coverage in the existing `tests/` integration project. + +## File Structure + +- `api/pkg/entities/message_thread.go`: persisted read fields. +- `api/pkg/entities/message_thread_test.go`: schema-default regression tests. +- `api/pkg/repositories/message_thread_repository.go`: field-scoped update parameter types and interface methods. +- `api/pkg/repositories/gorm_message_thread_repository.go`: transactional activity updates, conditional unread transition, partial user-status update, and partial deleted-message update. +- `api/pkg/repositories/gorm_message_thread_repository_test.go`: update-map ownership tests. +- `api/pkg/services/message_thread_service.go`: read-state business rules and repository orchestration. +- `api/pkg/services/message_thread_service_test.go`: service behavior with a repository stub. +- `api/pkg/requests/message_thread_update_request.go`: optional archive/read request fields. +- `api/pkg/requests/message_thread_update_request_test.go`: request-to-service conversion tests. +- `api/pkg/validators/message_thread_handler_validator.go`: require at least one update field. +- `api/pkg/validators/message_thread_handler_validator_test.go`: empty-payload and valid-payload tests. +- `api/pkg/responses/message_thead_responses.go`: single-thread Swagger response. +- `api/pkg/handlers/message_thread_handler.go`: not-found mapping and corrected Swagger response. +- `api/pkg/listeners/message_thread_listener.go`: incoming SMS and missed-call unread updates. +- `api/pkg/listeners/websocket_listener.go`: missed-call Pusher publication. +- `api/pkg/listeners/read_receipts_test_helpers_test.go`: listener logger and repository test doubles. +- `api/pkg/listeners/message_thread_listener_test.go`: listener route and payload tests. +- `api/pkg/listeners/websocket_listener_test.go`: missed-call route registration test. +- `api/docs/docs.go`, `api/docs/swagger.json`, `api/docs/swagger.yaml`: regenerated API documentation. +- `web/shared/types/api.ts`: regenerated thread/request types. +- `web/app/stores/threads.ts`: automatic read update and local state replacement. +- `web/app/pages/threads/[id]/index.vue`: non-blocking read calls and missed-call realtime refresh. +- `web/app/components/MessageThread.vue`: unread visual treatment. +- `tests/read_receipts_test.go`: Docker-stack read/unread lifecycle coverage. +- `tests/README.md`: integration coverage documentation. + +--- + +### Task 1: Add the read-state API contract + +**Files:** + +- Modify: `api/pkg/entities/message_thread.go` +- Create: `api/pkg/entities/message_thread_test.go` +- Modify: `api/pkg/requests/message_thread_update_request.go` +- Create: `api/pkg/requests/message_thread_update_request_test.go` +- Modify: `api/pkg/validators/message_thread_handler_validator.go` +- Create: `api/pkg/validators/message_thread_handler_validator_test.go` +- Modify: `api/pkg/responses/message_thead_responses.go` +- Modify: `api/pkg/services/message_thread_service.go` + +**Interfaces:** + +- Produces: `MessageThread.IsRead bool`, `MessageThread.LastReadAt time.Time` +- Produces: `MessageThreadUpdate.IsArchived *bool`, `MessageThreadUpdate.IsRead *bool` +- Produces: `MessageThreadStatusParams.IsArchived *bool`, `MessageThreadStatusParams.IsRead *bool` +- Produces: `responses.MessageThreadResponse` + +- [ ] **Step 1: Write failing entity schema tests** + +Create `api/pkg/entities/message_thread_test.go`: + +```go +package entities + +import ( + "reflect" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestMessageThreadReadFieldsHaveBackwardCompatibleDefaults(t *testing.T) { + threadType := reflect.TypeOf(MessageThread{}) + + isRead, ok := threadType.FieldByName("IsRead") + require.True(t, ok) + assert.Contains(t, isRead.Tag.Get("gorm"), "not null") + assert.Contains(t, isRead.Tag.Get("gorm"), "default:true") + assert.Equal(t, "is_read", isRead.Tag.Get("json")) + + lastReadAt, ok := threadType.FieldByName("LastReadAt") + require.True(t, ok) + assert.Contains(t, lastReadAt.Tag.Get("gorm"), "not null") + assert.Contains(t, lastReadAt.Tag.Get("gorm"), "default:CURRENT_TIMESTAMP") + assert.Equal(t, "-", lastReadAt.Tag.Get("json")) +} +``` + +- [ ] **Step 2: Write failing request and validator tests** + +Create `api/pkg/requests/message_thread_update_request_test.go`: + +```go +package requests + +import ( + "testing" + + "github.com/NdoleStudio/httpsms/pkg/entities" + "github.com/google/uuid" + "github.com/stretchr/testify/assert" +) + +func TestMessageThreadUpdateToUpdateParamsPreservesOptionalFields(t *testing.T) { + threadID := uuid.New() + isRead := true + input := MessageThreadUpdate{ + MessageThreadID: threadID.String(), + IsRead: &isRead, + } + + params := input.ToUpdateParams(entities.UserID("user-id")) + + assert.Equal(t, threadID, params.MessageThreadID) + assert.Equal(t, entities.UserID("user-id"), params.UserID) + assert.Nil(t, params.IsArchived) + assert.Same(t, &isRead, params.IsRead) +} +``` + +Create `api/pkg/validators/message_thread_handler_validator_test.go`: + +```go +package validators + +import ( + "context" + "testing" + + "github.com/NdoleStudio/httpsms/pkg/requests" + "github.com/google/uuid" + "github.com/stretchr/testify/assert" +) + +func TestValidateUpdateRequiresAtLeastOneStatusField(t *testing.T) { + validator := &MessageThreadHandlerValidator{} + request := requests.MessageThreadUpdate{ + MessageThreadID: uuid.NewString(), + } + + errors := validator.ValidateUpdate(context.Background(), request) + + assert.NotEmpty(t, errors.Get("payload")) +} + +func TestValidateUpdateAcceptsReadOnlyUpdate(t *testing.T) { + validator := &MessageThreadHandlerValidator{} + isRead := true + request := requests.MessageThreadUpdate{ + MessageThreadID: uuid.NewString(), + IsRead: &isRead, + } + + errors := validator.ValidateUpdate(context.Background(), request) + + assert.Empty(t, errors) +} +``` + +- [ ] **Step 3: Run the focused tests and confirm they fail** + +Run: + +```powershell +Set-Location 'C:\Users\achoa\Work\NdoleStudio\httpsms-read-receipts\api' +go test ./pkg/entities ./pkg/requests ./pkg/validators +``` + +Expected: compile failures because the read fields and optional request fields do not exist. + +- [ ] **Step 4: Add the persisted fields and optional request contract** + +Add to `entities.MessageThread` after `IsArchived`: + +```go +IsRead bool `json:"is_read" gorm:"not null;default:true" example:"true"` +LastReadAt time.Time `json:"-" gorm:"not null;default:CURRENT_TIMESTAMP"` +``` + +Change `requests.MessageThreadUpdate` and its conversion: + +```go +type MessageThreadUpdate struct { + request + IsArchived *bool `json:"is_archived,omitempty" example:"true"` + IsRead *bool `json:"is_read,omitempty" example:"true"` + + MessageThreadID string `json:"messageThreadID" swaggerignore:"true"` +} + +func (input *MessageThreadUpdate) ToUpdateParams(userID entities.UserID) services.MessageThreadStatusParams { + return services.MessageThreadStatusParams{ + UserID: userID, + MessageThreadID: uuid.MustParse(input.MessageThreadID), + IsArchived: input.IsArchived, + IsRead: input.IsRead, + } +} +``` + +Change the service parameter type: + +```go +type MessageThreadStatusParams struct { + IsArchived *bool + IsRead *bool + UserID entities.UserID + MessageThreadID uuid.UUID +} +``` + +After `v.ValidateStruct()` in `ValidateUpdate`, add an explicit payload check: + +```go +errors := v.ValidateStruct() +if request.IsArchived == nil && request.IsRead == nil { + errors.Add("payload", "at least one of is_archived or is_read is required") +} +return errors +``` + +Add a single-thread response beside `MessageThreadsResponse`: + +```go +// MessageThreadResponse is the payload containing entities.MessageThread +type MessageThreadResponse struct { + response + Data entities.MessageThread `json:"data"` +} +``` + +- [ ] **Step 5: Run focused tests** + +Run: + +```powershell +go test ./pkg/entities ./pkg/requests ./pkg/validators +``` + +Expected: PASS. + +- [ ] **Step 6: Commit the contract** + +```powershell +git add api/pkg/entities/message_thread.go api/pkg/entities/message_thread_test.go api/pkg/requests/message_thread_update_request.go api/pkg/requests/message_thread_update_request_test.go api/pkg/validators/message_thread_handler_validator.go api/pkg/validators/message_thread_handler_validator_test.go api/pkg/responses/message_thead_responses.go api/pkg/services/message_thread_service.go +@' +feat(api): add thread read state contract + +Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> +Copilot-Session: bf00a0ac-e11f-4015-b295-3cdd9b491229 +'@ | git commit -F - +``` + +### Task 2: Add atomic thread persistence operations + +**Files:** + +- Modify: `api/pkg/repositories/message_thread_repository.go` +- Modify: `api/pkg/repositories/gorm_message_thread_repository.go` +- Create: `api/pkg/repositories/gorm_message_thread_repository_test.go` + +**Interfaces:** + +- Produces: `repositories.MessageThreadActivityUpdate` +- Produces: `repositories.MessageThreadStatusUpdate` +- Produces: `repositories.MessageThreadDeletedUpdate` +- Produces: `MessageThreadRepository.UpdateActivity`, `UpdateStatus`, `UpdateAfterDeletedMessage` +- Removes: `MessageThreadRepository.Update` + +- [ ] **Step 1: Write failing update-map ownership tests** + +Create `api/pkg/repositories/gorm_message_thread_repository_test.go`: + +```go +package repositories + +import ( + "testing" + "time" + + "github.com/NdoleStudio/httpsms/pkg/entities" + "github.com/google/uuid" + "github.com/stretchr/testify/assert" +) + +func TestMessageThreadActivityUpdatesOwnOnlyMessageColumns(t *testing.T) { + messageID := uuid.New() + updates := messageThreadActivityUpdates(MessageThreadActivityUpdate{ + Timestamp: time.Date(2026, 7, 18, 7, 0, 0, 0, time.UTC), + MessageID: messageID, + Content: "hello", + Status: entities.MessageStatusReceived, + }) + + assert.Equal(t, map[string]any{ + "order_timestamp": time.Date(2026, 7, 18, 7, 0, 0, 0, time.UTC), + "last_message_id": messageID, + "last_message_content": "hello", + "status": entities.MessageStatusReceived, + }, updates) + assert.NotContains(t, updates, "is_read") + assert.NotContains(t, updates, "is_archived") + assert.NotContains(t, updates, "last_read_at") +} + +func TestMessageThreadStatusUpdatesReadOnly(t *testing.T) { + isRead := true + readAt := time.Date(2026, 7, 18, 7, 1, 0, 0, time.UTC) + + updates := messageThreadStatusUpdates(MessageThreadStatusUpdate{ + IsRead: &isRead, + ReadAt: readAt, + }) + + assert.Equal(t, map[string]any{ + "is_read": true, + "last_read_at": readAt, + }, updates) + assert.NotContains(t, updates, "is_archived") +} + +func TestMessageThreadStatusUpdatesArchiveOnly(t *testing.T) { + isArchived := true + + updates := messageThreadStatusUpdates(MessageThreadStatusUpdate{ + IsArchived: &isArchived, + }) + + assert.Equal(t, map[string]any{"is_archived": true}, updates) + assert.NotContains(t, updates, "is_read") + assert.NotContains(t, updates, "last_read_at") +} +``` + +- [ ] **Step 2: Run the repository test and confirm it fails** + +Run: + +```powershell +go test ./pkg/repositories -run MessageThread -v +``` + +Expected: compile failures because the parameter types and helper functions do not exist. + +- [ ] **Step 3: Define repository update parameters and methods** + +In `message_thread_repository.go`, add: + +```go +type MessageThreadActivityUpdate struct { + MessageThreadID uuid.UUID + UserID entities.UserID + Timestamp time.Time + MessageID uuid.UUID + Content string + Status entities.MessageStatus + MarksUnread bool + EventTimestamp time.Time +} + +type MessageThreadStatusUpdate struct { + IsArchived *bool + IsRead *bool + ReadAt time.Time +} + +type MessageThreadDeletedUpdate struct { + MessageThreadID uuid.UUID + UserID entities.UserID + LastMessageID *uuid.UUID + LastMessageContent *string + LastMessageStatus entities.MessageStatus +} +``` + +Add the `time` import. Replace `Update` and the old deleted-message method in the interface with: + +```go +UpdateActivity(ctx context.Context, params MessageThreadActivityUpdate) error +UpdateStatus(ctx context.Context, userID entities.UserID, messageThreadID uuid.UUID, params MessageThreadStatusUpdate) error +UpdateAfterDeletedMessage(ctx context.Context, params MessageThreadDeletedUpdate) error +``` + +- [ ] **Step 4: Implement field-scoped GORM updates** + +Add these helpers to `gorm_message_thread_repository.go`: + +```go +func messageThreadActivityUpdates(params MessageThreadActivityUpdate) map[string]any { + return map[string]any{ + "order_timestamp": params.Timestamp, + "last_message_id": params.MessageID, + "last_message_content": params.Content, + "status": params.Status, + } +} + +func messageThreadStatusUpdates(params MessageThreadStatusUpdate) map[string]any { + updates := make(map[string]any) + if params.IsArchived != nil { + updates["is_archived"] = *params.IsArchived + } + if params.IsRead != nil { + updates["is_read"] = *params.IsRead + if *params.IsRead { + updates["last_read_at"] = params.ReadAt + } + } + return updates +} +``` + +Replace the full-row `Update` implementation with: + +```go +func (repository *gormMessageThreadRepository) UpdateActivity(ctx context.Context, params MessageThreadActivityUpdate) error { + ctx, span := repository.tracer.Start(ctx) + defer span.End() + + err := repository.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { + query := tx.Model(&entities.MessageThread{}). + Where("user_id = ?", params.UserID). + Where("id = ?", params.MessageThreadID) + + result := query.Updates(messageThreadActivityUpdates(params)) + if result.Error != nil { + return result.Error + } + if result.RowsAffected == 0 { + return stacktrace.PropagateWithCode( + gorm.ErrRecordNotFound, + ErrCodeNotFound, + fmt.Sprintf("thread with id [%s] not found", params.MessageThreadID), + ) + } + + if !params.MarksUnread { + return nil + } + + return tx.Model(&entities.MessageThread{}). + Where("user_id = ?", params.UserID). + Where("id = ?", params.MessageThreadID). + Where("last_read_at < ?", params.EventTimestamp). + Update("is_read", false). + Error + }) + if err != nil { + msg := fmt.Sprintf("cannot update message activity for thread [%s]", params.MessageThreadID) + return repository.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCode(err, stacktrace.GetCode(err), msg)) + } + return nil +} + +func (repository *gormMessageThreadRepository) UpdateStatus( + ctx context.Context, + userID entities.UserID, + messageThreadID uuid.UUID, + params MessageThreadStatusUpdate, +) error { + ctx, span := repository.tracer.Start(ctx) + defer span.End() + + result := repository.db.WithContext(ctx). + Model(&entities.MessageThread{}). + Where("user_id = ?", userID). + Where("id = ?", messageThreadID). + Updates(messageThreadStatusUpdates(params)) + if result.Error != nil { + msg := fmt.Sprintf("cannot update status for thread [%s]", messageThreadID) + return repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(result.Error, msg)) + } + if result.RowsAffected == 0 { + msg := fmt.Sprintf("thread with id [%s] not found", messageThreadID) + return repository.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCode(gorm.ErrRecordNotFound, ErrCodeNotFound, msg)) + } + return nil +} + +func (repository *gormMessageThreadRepository) UpdateAfterDeletedMessage(ctx context.Context, params MessageThreadDeletedUpdate) error { + ctx, span := repository.tracer.Start(ctx) + defer span.End() + + result := repository.db.WithContext(ctx). + Model(&entities.MessageThread{}). + Where("user_id = ?", params.UserID). + Where("id = ?", params.MessageThreadID). + Updates(map[string]any{ + "last_message_id": params.LastMessageID, + "last_message_content": params.LastMessageContent, + "status": params.LastMessageStatus, + }) + if result.Error != nil { + msg := fmt.Sprintf("cannot update deleted-message metadata for thread [%s]", params.MessageThreadID) + return repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(result.Error, msg)) + } + return nil +} +``` + +Delete the old `Save(thread)` method and the old unused `UpdateAfterDeletedMessage(userID, messageID)` implementation. + +- [ ] **Step 5: Run repository tests** + +Run: + +```powershell +go test ./pkg/repositories -run MessageThread -v +``` + +Expected: PASS. + +- [ ] **Step 6: Commit atomic persistence** + +```powershell +git add api/pkg/repositories/message_thread_repository.go api/pkg/repositories/gorm_message_thread_repository.go api/pkg/repositories/gorm_message_thread_repository_test.go +@' +refactor(api): make thread updates atomic + +Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> +Copilot-Session: bf00a0ac-e11f-4015-b295-3cdd9b491229 +'@ | git commit -F - +``` + +### Task 3: Apply read rules in services and listeners + +**Files:** + +- Modify: `api/pkg/services/message_thread_service.go` +- Create: `api/pkg/services/message_thread_service_test.go` +- Modify: `api/pkg/listeners/message_thread_listener.go` +- Create: `api/pkg/listeners/read_receipts_test_helpers_test.go` +- Create: `api/pkg/listeners/message_thread_listener_test.go` +- Modify: `api/pkg/listeners/websocket_listener.go` +- Create: `api/pkg/listeners/websocket_listener_test.go` + +**Interfaces:** + +- Consumes: repository update types and methods from Task 2. +- Produces: `MessageThreadUpdateParams.MarksUnread bool` +- Produces: `MessageThreadUpdateParams.EventTimestamp time.Time` +- Produces: missed-call thread and websocket listeners. + +- [ ] **Step 1: Write failing service behavior tests** + +Create `api/pkg/services/message_thread_service_test.go`. Implement a repository stub with all interface methods returning zero values by default and function hooks for the methods under test: + +```go +package services + +import ( + "context" + "testing" + "time" + + "github.com/NdoleStudio/httpsms/pkg/entities" + "github.com/NdoleStudio/httpsms/pkg/repositories" + "github.com/NdoleStudio/httpsms/pkg/telemetry" + "github.com/google/uuid" + "github.com/palantir/stacktrace" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "gorm.io/gorm" +) + +type messageThreadRepositoryStub struct { + loadByOwnerContact func(context.Context, entities.UserID, string, string) (*entities.MessageThread, error) + load func(context.Context, entities.UserID, uuid.UUID) (*entities.MessageThread, error) + store func(context.Context, *entities.MessageThread) error + updateActivity func(context.Context, repositories.MessageThreadActivityUpdate) error + updateStatus func(context.Context, entities.UserID, uuid.UUID, repositories.MessageThreadStatusUpdate) error +} + +func (stub *messageThreadRepositoryStub) Store(ctx context.Context, thread *entities.MessageThread) error { + if stub.store != nil { + return stub.store(ctx, thread) + } + return nil +} + +func (stub *messageThreadRepositoryStub) UpdateActivity(ctx context.Context, params repositories.MessageThreadActivityUpdate) error { + if stub.updateActivity != nil { + return stub.updateActivity(ctx, params) + } + return nil +} + +func (stub *messageThreadRepositoryStub) UpdateStatus(ctx context.Context, userID entities.UserID, threadID uuid.UUID, params repositories.MessageThreadStatusUpdate) error { + if stub.updateStatus != nil { + return stub.updateStatus(ctx, userID, threadID, params) + } + return nil +} + +func (stub *messageThreadRepositoryStub) UpdateAfterDeletedMessage(context.Context, repositories.MessageThreadDeletedUpdate) error { + return nil +} + +func (stub *messageThreadRepositoryStub) LoadByOwnerContact(ctx context.Context, userID entities.UserID, owner string, contact string) (*entities.MessageThread, error) { + return stub.loadByOwnerContact(ctx, userID, owner, contact) +} + +func (stub *messageThreadRepositoryStub) Load(ctx context.Context, userID entities.UserID, id uuid.UUID) (*entities.MessageThread, error) { + return stub.load(ctx, userID, id) +} + +func (stub *messageThreadRepositoryStub) Index(context.Context, entities.UserID, string, bool, repositories.IndexParams) (*[]entities.MessageThread, error) { + threads := []entities.MessageThread{} + return &threads, nil +} + +func (stub *messageThreadRepositoryStub) Delete(context.Context, entities.UserID, uuid.UUID) error { + return nil +} + +func (stub *messageThreadRepositoryStub) DeleteAllForUser(context.Context, entities.UserID) error { + return nil +} + +func newMessageThreadServiceForTest(repository repositories.MessageThreadRepository) *MessageThreadService { + logger := &noopLogger{} + tracer := telemetry.NewOtelLogger("test", logger) + return NewMessageThreadService(logger, tracer, repository, nil) +} +``` + +Add these tests below the stub: + +```go +func TestUpdateThreadPassesUnreadWatermarkForInboundActivity(t *testing.T) { + threadID := uuid.New() + eventTimestamp := time.Date(2026, 7, 18, 7, 0, 0, 0, time.UTC) + var captured repositories.MessageThreadActivityUpdate + repository := &messageThreadRepositoryStub{ + loadByOwnerContact: func(context.Context, entities.UserID, string, string) (*entities.MessageThread, error) { + return &entities.MessageThread{ID: threadID}, nil + }, + updateActivity: func(_ context.Context, params repositories.MessageThreadActivityUpdate) error { + captured = params + return nil + }, + } + + service := newMessageThreadServiceForTest(repository) + err := service.UpdateThread(context.Background(), MessageThreadUpdateParams{ + UserID: entities.UserID("user-id"), + Owner: "+18005550199", + Contact: "+18005550100", + MessageID: uuid.New(), + Content: "hello", + Status: entities.MessageStatusReceived, + Timestamp: eventTimestamp, + MarksUnread: true, + EventTimestamp: eventTimestamp, + }) + + require.NoError(t, err) + assert.True(t, captured.MarksUnread) + assert.Equal(t, eventTimestamp, captured.EventTimestamp) +} + +func TestUpdateThreadPreservesReadStateForOutboundActivity(t *testing.T) { + var captured repositories.MessageThreadActivityUpdate + repository := &messageThreadRepositoryStub{ + loadByOwnerContact: func(context.Context, entities.UserID, string, string) (*entities.MessageThread, error) { + return &entities.MessageThread{ID: uuid.New(), IsRead: false}, nil + }, + updateActivity: func(_ context.Context, params repositories.MessageThreadActivityUpdate) error { + captured = params + return nil + }, + } + + service := newMessageThreadServiceForTest(repository) + err := service.UpdateThread(context.Background(), MessageThreadUpdateParams{ + UserID: entities.UserID("user-id"), + Owner: "+18005550199", + Contact: "+18005550100", + MessageID: uuid.New(), + Content: "outbound", + Status: entities.MessageStatusSent, + Timestamp: time.Now().UTC(), + }) + + require.NoError(t, err) + assert.False(t, captured.MarksUnread) +} + +func TestCreateThreadSetsReadStateFromActivityDirection(t *testing.T) { + tests := []struct { + name string + marksUnread bool + wantRead bool + }{ + {name: "inbound", marksUnread: true, wantRead: false}, + {name: "outbound", marksUnread: false, wantRead: true}, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + var stored *entities.MessageThread + repository := &messageThreadRepositoryStub{ + loadByOwnerContact: func(context.Context, entities.UserID, string, string) (*entities.MessageThread, error) { + return nil, stacktrace.PropagateWithCode(gorm.ErrRecordNotFound, repositories.ErrCodeNotFound, "not found") + }, + store: func(_ context.Context, thread *entities.MessageThread) error { + stored = thread + return nil + }, + } + + service := newMessageThreadServiceForTest(repository) + err := service.UpdateThread(context.Background(), MessageThreadUpdateParams{ + UserID: entities.UserID("user-id"), + Owner: "+18005550199", + Contact: "+18005550100", + MessageID: uuid.New(), + Content: "hello", + Status: entities.MessageStatusReceived, + Timestamp: time.Now().UTC(), + MarksUnread: test.marksUnread, + }) + + require.NoError(t, err) + require.NotNil(t, stored) + assert.Equal(t, test.wantRead, stored.IsRead) + assert.False(t, stored.LastReadAt.IsZero()) + }) + } +} + +func TestUpdateStatusChangesOnlyRequestedState(t *testing.T) { + threadID := uuid.New() + isRead := true + var captured repositories.MessageThreadStatusUpdate + repository := &messageThreadRepositoryStub{ + updateStatus: func(_ context.Context, _ entities.UserID, _ uuid.UUID, params repositories.MessageThreadStatusUpdate) error { + captured = params + return nil + }, + load: func(context.Context, entities.UserID, uuid.UUID) (*entities.MessageThread, error) { + return &entities.MessageThread{ID: threadID, IsArchived: true, IsRead: true}, nil + }, + } + + service := newMessageThreadServiceForTest(repository) + thread, err := service.UpdateStatus(context.Background(), MessageThreadStatusParams{ + UserID: entities.UserID("user-id"), + MessageThreadID: threadID, + IsRead: &isRead, + }) + + require.NoError(t, err) + assert.Nil(t, captured.IsArchived) + assert.Same(t, &isRead, captured.IsRead) + assert.False(t, captured.ReadAt.IsZero()) + assert.True(t, thread.IsArchived) +} + +func TestUpdateStatusPreservesNotFoundCode(t *testing.T) { + repository := &messageThreadRepositoryStub{ + updateStatus: func(context.Context, entities.UserID, uuid.UUID, repositories.MessageThreadStatusUpdate) error { + return stacktrace.PropagateWithCode(gorm.ErrRecordNotFound, repositories.ErrCodeNotFound, "not found") + }, + } + + service := newMessageThreadServiceForTest(repository) + isRead := true + _, err := service.UpdateStatus(context.Background(), MessageThreadStatusParams{ + UserID: entities.UserID("user-id"), + MessageThreadID: uuid.New(), + IsRead: &isRead, + }) + + assert.Equal(t, repositories.ErrCodeNotFound, stacktrace.GetCode(err)) +} +``` + +- [ ] **Step 2: Run service tests and confirm they fail** + +Run: + +```powershell +go test ./pkg/services -run 'Test(UpdateThread|CreateThread|UpdateStatus)' -v +``` + +Expected: compile failures because the new update parameters and repository calls are not wired. + +- [ ] **Step 3: Implement service read rules** + +Extend `MessageThreadUpdateParams`: + +```go +MarksUnread bool +EventTimestamp time.Time +``` + +Replace the existing full-row update in `UpdateThread` with: + +```go +if err = service.repository.UpdateActivity(ctx, repositories.MessageThreadActivityUpdate{ + MessageThreadID: thread.ID, + UserID: params.UserID, + Timestamp: params.Timestamp, + MessageID: params.MessageID, + Content: params.Content, + Status: params.Status, + MarksUnread: params.MarksUnread, + EventTimestamp: params.EventTimestamp, +}); err != nil { + msg := fmt.Sprintf("cannot update message thread with id [%s] after adding message [%s]", thread.ID, params.MessageID) + return service.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCode(err, stacktrace.GetCode(err), msg)) +} +``` + +In `createThread`, create one `now := time.Now().UTC()` and initialize: + +```go +IsRead: !params.MarksUnread, +LastReadAt: now, +CreatedAt: now, +UpdatedAt: now, +``` + +Replace `UpdateStatus` with a partial repository update followed by a reload: + +```go +func (service *MessageThreadService) UpdateStatus(ctx context.Context, params MessageThreadStatusParams) (*entities.MessageThread, error) { + ctx, span := service.tracer.Start(ctx) + defer span.End() + + update := repositories.MessageThreadStatusUpdate{ + IsArchived: params.IsArchived, + IsRead: params.IsRead, + ReadAt: time.Now().UTC(), + } + if err := service.repository.UpdateStatus(ctx, params.UserID, params.MessageThreadID, update); err != nil { + msg := fmt.Sprintf("cannot update message thread with id [%s]", params.MessageThreadID) + return nil, service.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCode(err, stacktrace.GetCode(err), msg)) + } + + thread, err := service.repository.Load(ctx, params.UserID, params.MessageThreadID) + if err != nil { + msg := fmt.Sprintf("cannot reload message thread with id [%s]", params.MessageThreadID) + return nil, service.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCode(err, stacktrace.GetCode(err), msg)) + } + return thread, nil +} +``` + +In `UpdateAfterDeletedMessage`, replace the full-row update with: + +```go +if err = service.repository.UpdateAfterDeletedMessage(ctx, repositories.MessageThreadDeletedUpdate{ + MessageThreadID: thread.ID, + UserID: thread.UserID, + LastMessageID: payload.PreviousMessageID, + LastMessageContent: payload.PreviousMessageContent, + LastMessageStatus: *payload.PreviousMessageStatus, +}); err != nil { + msg := fmt.Sprintf("cannot update thread with ID [%s] for user with ID [%s]", thread.ID, thread.UserID) + return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg)) +} +``` + +- [ ] **Step 4: Add inbound and missed-call listener behavior** + +In `NewMessageThreadListener`, register: + +```go +events.MessageCallMissed: l.OnMessageCallMissed, +``` + +In `OnMessagePhoneReceived`, add: + +```go +MarksUnread: true, +EventTimestamp: event.Time(), +``` + +Add: + +```go +func (listener *MessageThreadListener) OnMessageCallMissed(ctx context.Context, event cloudevents.Event) error { + ctx, span := listener.tracer.Start(ctx) + defer span.End() + + var payload events.MessageCallMissedPayload + if err := event.DataAs(&payload); err != nil { + msg := fmt.Sprintf("cannot decode [%s] into [%T]", event.Data(), payload) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg)) + } + + params := services.MessageThreadUpdateParams{ + Owner: payload.Owner, + Contact: payload.Contact, + UserID: payload.UserID, + Status: entities.MessageStatusReceived, + Timestamp: payload.Timestamp, + Content: "Missed phone call", + MessageID: payload.MessageID, + MarksUnread: true, + EventTimestamp: event.Time(), + } + if err := listener.service.UpdateThread(ctx, params); err != nil { + msg := fmt.Sprintf("cannot update thread for missed call [%s] on event [%s]", payload.MessageID, event.ID()) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg)) + } + return nil +} +``` + +In `NewWebsocketListener`, register: + +```go +events.MessageCallMissed: l.onMessageCallMissed, +``` + +Add: + +```go +func (listener *WebsocketListener) onMessageCallMissed(ctx context.Context, event cloudevents.Event) error { + ctx, span, _ := listener.tracer.StartWithLogger(ctx, listener.logger) + defer span.End() + + var payload events.MessageCallMissedPayload + if err := event.DataAs(&payload); err != nil { + msg := fmt.Sprintf("cannot decode [%s] into [%T]", event.Data(), payload) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg)) + } + + if err := listener.client.Trigger(payload.UserID.String(), event.Type(), event.ID()); err != nil { + msg := fmt.Sprintf("cannot trigger websocket [%s] event with ID [%s] for user with ID [%s]", event.Type(), event.ID(), payload.UserID) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg)) + } + return nil +} +``` + +- [ ] **Step 5: Add listener tests** + +Create `api/pkg/listeners/read_receipts_test_helpers_test.go`: + +```go +package listeners + +import ( + "context" + + "github.com/NdoleStudio/httpsms/pkg/entities" + "github.com/NdoleStudio/httpsms/pkg/repositories" + "github.com/NdoleStudio/httpsms/pkg/telemetry" + "github.com/google/uuid" + "go.opentelemetry.io/otel/trace" +) + +type noopListenerLogger struct{} + +func (logger *noopListenerLogger) Error(error) {} +func (logger *noopListenerLogger) WithService(string) telemetry.Logger { return logger } +func (logger *noopListenerLogger) WithString(string, string) telemetry.Logger { return logger } +func (logger *noopListenerLogger) WithSpan(trace.SpanContext) telemetry.Logger { return logger } +func (logger *noopListenerLogger) Trace(string) {} +func (logger *noopListenerLogger) Info(string) {} +func (logger *noopListenerLogger) Warn(error) {} +func (logger *noopListenerLogger) Debug(string) {} +func (logger *noopListenerLogger) Fatal(error) {} +func (logger *noopListenerLogger) Printf(string, ...interface{}) {} + +type listenerMessageThreadRepository struct { + activity repositories.MessageThreadActivityUpdate +} + +func (repository *listenerMessageThreadRepository) Store(context.Context, *entities.MessageThread) error { + return nil +} + +func (repository *listenerMessageThreadRepository) UpdateActivity(_ context.Context, params repositories.MessageThreadActivityUpdate) error { + repository.activity = params + return nil +} + +func (repository *listenerMessageThreadRepository) UpdateStatus(context.Context, entities.UserID, uuid.UUID, repositories.MessageThreadStatusUpdate) error { + return nil +} + +func (repository *listenerMessageThreadRepository) UpdateAfterDeletedMessage(context.Context, repositories.MessageThreadDeletedUpdate) error { + return nil +} + +func (repository *listenerMessageThreadRepository) LoadByOwnerContact(context.Context, entities.UserID, string, string) (*entities.MessageThread, error) { + return &entities.MessageThread{ID: uuid.New()}, nil +} + +func (repository *listenerMessageThreadRepository) Load(context.Context, entities.UserID, uuid.UUID) (*entities.MessageThread, error) { + return &entities.MessageThread{}, nil +} + +func (repository *listenerMessageThreadRepository) Index(context.Context, entities.UserID, string, bool, repositories.IndexParams) (*[]entities.MessageThread, error) { + threads := []entities.MessageThread{} + return &threads, nil +} + +func (repository *listenerMessageThreadRepository) Delete(context.Context, entities.UserID, uuid.UUID) error { + return nil +} + +func (repository *listenerMessageThreadRepository) DeleteAllForUser(context.Context, entities.UserID) error { + return nil +} +``` + +Create `api/pkg/listeners/message_thread_listener_test.go`: + +```go +package listeners + +import ( + "context" + "testing" + "time" + + "github.com/NdoleStudio/httpsms/pkg/entities" + "github.com/NdoleStudio/httpsms/pkg/events" + "github.com/NdoleStudio/httpsms/pkg/services" + "github.com/NdoleStudio/httpsms/pkg/telemetry" + cloudevents "github.com/cloudevents/sdk-go/v2" + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestMessageThreadListenerMarksMissedCallUnread(t *testing.T) { + repository := &listenerMessageThreadRepository{} + logger := &noopListenerLogger{} + tracer := telemetry.NewOtelLogger("test", logger) + service := services.NewMessageThreadService(logger, tracer, repository, nil) + _, routes := NewMessageThreadListener(logger, tracer, service) + + event := cloudevents.NewEvent() + event.SetID(uuid.NewString()) + event.SetSource("/v1/messages/call-missed") + event.SetType(events.MessageCallMissed) + event.SetTime(time.Date(2026, 7, 18, 7, 0, 0, 0, time.UTC)) + require.NoError(t, event.SetData(cloudevents.ApplicationJSON, events.MessageCallMissedPayload{ + MessageID: uuid.New(), + UserID: entities.UserID("user-id"), + Owner: "+18005550199", + Contact: "+18005550100", + Timestamp: time.Date(2026, 7, 18, 6, 59, 0, 0, time.UTC), + })) + + err := routes[events.MessageCallMissed](context.Background(), event) + + require.NoError(t, err) + assert.True(t, repository.activity.MarksUnread) + assert.Equal(t, "Missed phone call", repository.activity.Content) + assert.Equal(t, event.Time(), repository.activity.EventTimestamp) +} +``` + +Create `api/pkg/listeners/websocket_listener_test.go`: + +```go +package listeners + +import ( + "testing" + + "github.com/NdoleStudio/httpsms/pkg/events" + "github.com/NdoleStudio/httpsms/pkg/telemetry" + "github.com/pusher/pusher-http-go/v5" + "github.com/stretchr/testify/assert" +) + +func TestWebsocketListenerRegistersMissedCalls(t *testing.T) { + logger := &noopListenerLogger{} + tracer := telemetry.NewOtelLogger("test", logger) + _, routes := NewWebsocketListener(logger, tracer, &pusher.Client{}) + + assert.Contains(t, routes, events.MessageCallMissed) +} +``` + +- [ ] **Step 6: Run service and listener tests** + +Run: + +```powershell +go test ./pkg/services ./pkg/listeners -run 'MessageThread|MissedCall|UpdateStatus|CreateThread' -v +``` + +Expected: PASS. + +- [ ] **Step 7: Commit service and realtime behavior** + +```powershell +git add api/pkg/services/message_thread_service.go api/pkg/services/message_thread_service_test.go api/pkg/listeners/message_thread_listener.go api/pkg/listeners/read_receipts_test_helpers_test.go api/pkg/listeners/message_thread_listener_test.go api/pkg/listeners/websocket_listener.go api/pkg/listeners/websocket_listener_test.go +@' +feat(api): track unread inbound threads + +Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> +Copilot-Session: bf00a0ac-e11f-4015-b295-3cdd9b491229 +'@ | git commit -F - +``` + +### Task 4: Finish the HTTP behavior and regenerate Swagger + +**Files:** + +- Modify: `api/pkg/handlers/message_thread_handler.go` +- Modify: `api/docs/docs.go` +- Modify: `api/docs/swagger.json` +- Modify: `api/docs/swagger.yaml` + +**Interfaces:** + +- Consumes: `responses.MessageThreadResponse` from Task 1. +- Produces: 404 behavior for missing thread updates. +- Produces: Swagger schema fields `is_read`, optional `is_archived`, optional `is_read`. + +- [ ] **Step 1: Update the handler error mapping and annotations** + +Change the update annotation: + +```go +// @Success 200 {object} responses.MessageThreadResponse +// @Failure 404 {object} responses.NotFound +``` + +Change the error block: + +```go +thread, err := h.service.UpdateStatus(ctx, request.ToUpdateParams(h.userIDFomContext(c))) +if stacktrace.GetCode(err) == repositories.ErrCodeNotFound { + return h.responseNotFound(c, fmt.Sprintf("cannot find message thread with ID [%s]", request.MessageThreadID)) +} +if err != nil { + msg := fmt.Sprintf("cannot update message thread with params [%+#v]", request) + ctxLogger.Error(stacktrace.Propagate(err, msg)) + return h.responseInternalServerError(c) +} +``` + +- [ ] **Step 2: Regenerate Swagger** + +Run: + +```powershell +Set-Location 'C:\Users\achoa\Work\NdoleStudio\httpsms-read-receipts\api' +swag init --requiredByDefault --parseDependency --parseInternal +``` + +Expected: `api/docs/docs.go`, `api/docs/swagger.json`, and `api/docs/swagger.yaml` update successfully. + +- [ ] **Step 3: Run the full API suite** + +Run: + +```powershell +go test -vet=off ./pkg/entities ./pkg/repositories ./pkg/requests ./pkg/services ./pkg/validators ./pkg/listeners +go test -vet=off ./pkg/handlers -run '^$' +``` + +Expected: PASS. Vet is disabled because clean `origin/main` has unrelated Go +1.25 non-constant-format vet failures. Handler tests are compile-checked only +because the pre-existing suite expects an API server on `localhost:8000`; Task +7 provides the HTTP integration coverage against the Docker stack. + +- [ ] **Step 4: Commit HTTP and documentation changes** + +```powershell +git add api/pkg/handlers/message_thread_handler.go api/docs/docs.go api/docs/swagger.json api/docs/swagger.yaml +@' +docs(api): publish thread read state + +Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> +Copilot-Session: bf00a0ac-e11f-4015-b295-3cdd9b491229 +'@ | git commit -F - +``` + +### Task 5: Add automatic read updates to the web store + +**Files:** + +- Modify: `web/shared/types/api.ts` +- Modify: `web/app/stores/threads.ts` + +**Interfaces:** + +- Consumes: API `PUT /v1/message-threads/{id}` with `{ is_read: true }`. +- Produces: `markThreadRead(threadId: string, force?: boolean): Promise`. + +- [ ] **Step 1: Regenerate TypeScript API models** + +Run: + +```powershell +Set-Location 'C:\Users\achoa\Work\NdoleStudio\httpsms-read-receipts\web' +pnpm api:models +``` + +Expected: `EntitiesMessageThread.is_read: boolean` and optional fields on `RequestsMessageThreadUpdate`. + +- [ ] **Step 2: Add a typed local replacement helper** + +In `web/app/stores/threads.ts`, add: + +```ts +function replaceThread(updatedThread: EntitiesMessageThread) { + const index = threads.value.findIndex( + (thread) => thread.id === updatedThread.id, + ); + if (index !== -1) threads.value[index] = updatedThread; +} +``` + +- [ ] **Step 3: Implement non-silent automatic read persistence** + +Add: + +```ts +async function markThreadRead(threadId: string, force = false) { + const thread = threads.value.find((item) => item.id === threadId); + if (!thread) throw new Error(`Cannot find thread with id ${threadId}`); + if (!force && thread.is_read) return; + + try { + const response = await apiFetch<{ data: EntitiesMessageThread }>( + `/v1/message-threads/${threadId}`, + { + method: "PUT", + body: { is_read: true }, + }, + ); + replaceThread(response.data); + } catch (error) { + notificationsStore.addNotification({ + message: "The message thread could not be marked as read", + type: "error", + }); + await loadThreads(); + throw error; + } +} +``` + +Add `markThreadRead` to the returned store API. + +Keep archive updates independent: + +```ts +body: { is_archived: payload.isArchived }, +``` + +Do not send `is_read` from `updateThread`. + +- [ ] **Step 4: Run web static checks** + +Run: + +```powershell +pnpm lint:js +pnpm lint:prettier +``` + +Expected: PASS. + +- [ ] **Step 5: Commit store and generated types** + +```powershell +git add web/shared/types/api.ts web/app/stores/threads.ts +@' +feat(web): persist opened threads as read + +Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> +Copilot-Session: bf00a0ac-e11f-4015-b295-3cdd9b491229 +'@ | git commit -F - +``` + +### Task 6: Mark opened threads read and highlight unread threads + +**Files:** + +- Modify: `web/app/pages/threads/[id]/index.vue` +- Modify: `web/app/components/MessageThread.vue` + +**Interfaces:** + +- Consumes: `threadsStore.markThreadRead(threadId, force)`. +- Produces: automatic initial and realtime read updates. +- Produces: shared unread list styling on mobile and desktop. + +- [ ] **Step 1: Make read updates independent from message loading** + +In `web/app/pages/threads/[id]/index.vue`, add: + +```ts +async function markCurrentThreadRead(force = false) { + const threadId = route.params.id as string; + try { + await threadsStore.markThreadRead(threadId, force); + } catch (error) { + console.error(error); + } +} +``` + +At the start of `loadMessages`, after computing `threadId`, call without awaiting: + +```ts +void markCurrentThreadRead(); +``` + +This lets message loading continue even if the read update fails. + +- [ ] **Step 2: Force a newer read watermark after inbound realtime events** + +Change the incoming SMS binding: + +```ts +webhookChannel.bind("message.phone.received", () => { + if (!loadingMessages.value) { + void markCurrentThreadRead(true); + loadMessages(false); + } +}); +``` + +Add the missed-call binding with the same behavior: + +```ts +webhookChannel.bind("message.call.missed", () => { + if (!loadingMessages.value) { + void markCurrentThreadRead(true); + loadMessages(false); + } +}); +``` + +The forced call is required because the local thread may still say `is_read: +true` when the concurrent backend listener has not committed its unread update. + +- [ ] **Step 3: Add unread presentation** + +In `MessageThread.vue`, add the class binding to `v-list-item`: + +```vue +:class="{ 'message-thread--unread': !thread.is_read }" +``` + +Change title and subtitle class bindings: + +```vue + + {{ formatPhoneNumber(thread.contact) }} + + +``` + +Add: + +```vue + +``` + +- [ ] **Step 4: Run web validation** + +Run: + +```powershell +Set-Location 'C:\Users\achoa\Work\NdoleStudio\httpsms-read-receipts\web' +pnpm lint +pnpm run generate +``` + +Expected: both commands PASS. + +- [ ] **Step 5: Commit UI behavior** + +```powershell +git add web/app/pages/threads/[id]/index.vue web/app/components/MessageThread.vue +@' +feat(web): highlight unread message threads + +Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> +Copilot-Session: bf00a0ac-e11f-4015-b295-3cdd9b491229 +'@ | git commit -F - +``` + +### Task 7: Add integration coverage + +**Files:** + +- Create: `tests/read_receipts_test.go` +- Modify: `tests/README.md` + +**Interfaces:** + +- Consumes: thread index and update HTTP endpoints from Tasks 1-4. +- Consumes: incoming SMS, missed-call, and outbound message flows from Task 3. +- Produces: `TestMessageThreadReadReceipts`. + +- [ ] **Step 1: Write the end-to-end read-receipts test** + +Create `tests/read_receipts_test.go`: + +```go +package tests + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "testing" + "time" + + httpsms "github.com/NdoleStudio/httpsms-go" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +type integrationMessageThread struct { + ID string `json:"id"` + Contact string `json:"contact"` + IsRead bool `json:"is_read"` + LastMessageContent *string `json:"last_message_content"` +} + +func requestJSON( + ctx context.Context, + t *testing.T, + method string, + path string, + apiKey string, + payload any, + expectedStatus int, + output any, +) { + t.Helper() + + var body io.Reader + if payload != nil { + encoded, err := json.Marshal(payload) + require.NoError(t, err) + body = bytes.NewReader(encoded) + } + + request, err := http.NewRequestWithContext(ctx, method, apiBaseURL+path, body) + require.NoError(t, err) + request.Header.Set("x-api-key", apiKey) + request.Header.Set("Content-Type", "application/json") + + response, err := http.DefaultClient.Do(request) + require.NoError(t, err) + defer response.Body.Close() + + responseBody, err := io.ReadAll(response.Body) + require.NoError(t, err) + require.Equal(t, expectedStatus, response.StatusCode, "response: %s", string(responseBody)) + + if output != nil { + require.NoError(t, json.Unmarshal(responseBody, output)) + } +} + +func fetchMessageThreads(ctx context.Context, t *testing.T, owner string) []integrationMessageThread { + t.Helper() + + var response struct { + Data []integrationMessageThread `json:"data"` + } + path := fmt.Sprintf( + "/v1/message-threads?owner=%s&skip=0&limit=20&is_archived=false", + url.QueryEscape(owner), + ) + requestJSON(ctx, t, http.MethodGet, path, userAPIKey, nil, http.StatusOK, &response) + return response.Data +} + +func waitForMessageThread( + ctx context.Context, + t *testing.T, + owner string, + contact string, + timeout time.Duration, + matches func(integrationMessageThread) bool, +) integrationMessageThread { + t.Helper() + + deadline := time.Now().Add(timeout) + for time.Now().Before(deadline) { + for _, thread := range fetchMessageThreads(ctx, t, owner) { + if thread.Contact == contact && matches(thread) { + return thread + } + } + time.Sleep(500 * time.Millisecond) + } + + t.Fatalf("thread %s -> %s did not reach the expected state within %v", owner, contact, timeout) + return integrationMessageThread{} +} + +func markMessageThreadRead(ctx context.Context, t *testing.T, threadID string) integrationMessageThread { + t.Helper() + + var response struct { + Data integrationMessageThread `json:"data"` + } + requestJSON( + ctx, + t, + http.MethodPut, + "/v1/message-threads/"+threadID, + userAPIKey, + map[string]any{"is_read": true}, + http.StatusOK, + &response, + ) + return response.Data +} + +func TestMessageThreadReadReceipts(t *testing.T) { + ctx := context.Background() + phone := setupPhone(ctx, t, 60) + contact := randomPhoneNumber() + + requestJSON( + ctx, + t, + http.MethodPost, + "/v1/messages/receive", + phone.PhoneAPIKey, + map[string]any{ + "from": contact, + "to": phone.PhoneNumber, + "content": "Unread inbound message", + "encrypted": false, + "sim": "SIM1", + "timestamp": time.Now().UTC().Format(time.RFC3339), + }, + http.StatusOK, + nil, + ) + + thread := waitForMessageThread(ctx, t, phone.PhoneNumber, contact, 20*time.Second, func(thread integrationMessageThread) bool { + return !thread.IsRead + }) + assert.False(t, thread.IsRead) + + updated := markMessageThreadRead(ctx, t, thread.ID) + assert.True(t, updated.IsRead) + waitForMessageThread(ctx, t, phone.PhoneNumber, contact, 10*time.Second, func(thread integrationMessageThread) bool { + return thread.IsRead + }) + + requestJSON( + ctx, + t, + http.MethodPost, + "/v1/messages/calls/missed", + phone.PhoneAPIKey, + map[string]any{ + "from": contact, + "to": phone.PhoneNumber, + "sim": "SIM1", + "timestamp": time.Now().UTC().Format(time.RFC3339), + }, + http.StatusOK, + nil, + ) + + thread = waitForMessageThread(ctx, t, phone.PhoneNumber, contact, 20*time.Second, func(thread integrationMessageThread) bool { + return !thread.IsRead && + thread.LastMessageContent != nil && + *thread.LastMessageContent == "Missed phone call" + }) + assert.False(t, thread.IsRead) + + outboundContent := "Outbound activity preserves unread" + client := newAPIClient() + _, response, err := client.Messages.Send(ctx, &httpsms.MessageSendParams{ + From: phone.PhoneNumber, + To: contact, + Content: outboundContent, + }) + require.NoError(t, err) + require.Equal(t, http.StatusOK, response.HTTPResponse.StatusCode) + + thread = waitForMessageThread(ctx, t, phone.PhoneNumber, contact, 20*time.Second, func(thread integrationMessageThread) bool { + return thread.LastMessageContent != nil && + *thread.LastMessageContent == outboundContent + }) + assert.False(t, thread.IsRead, "outbound activity must not clear unread state") +} +``` + +- [ ] **Step 2: Run the test before implementation is complete** + +With the Docker integration stack running, execute: + +```powershell +Set-Location 'C:\Users\achoa\Work\NdoleStudio\httpsms-read-receipts\tests' +go test -v -timeout 120s -run TestMessageThreadReadReceipts ./... +``` + +Expected before the feature implementation: FAIL because thread responses do not expose/persist `is_read`. + +- [ ] **Step 3: Update integration-test documentation** + +Add to the Test Coverage checklist in `tests/README.md`: + +```markdown +- [x] **Message thread read receipts E2E** — Incoming SMS and missed calls mark a thread unread, the existing thread update endpoint marks it read, and outbound activity preserves unread state +``` + +- [ ] **Step 4: Run the integration test against the completed stack** + +Generate credentials, start the Docker stack, run the focused test, and tear down: + +```powershell +Set-Location 'C:\Users\achoa\Work\NdoleStudio\httpsms-read-receipts\tests' +& 'C:\Program Files\Git\bin\bash.exe' generate-firebase-credentials.sh +$env:FIREBASE_CREDENTIALS = (Get-Content firebase-credentials.json -Raw | ConvertFrom-Json | ConvertTo-Json -Compress) +docker compose up -d --build --wait +docker compose wait seed +Start-Sleep -Seconds 2 +try { + go test -v -timeout 120s -run TestMessageThreadReadReceipts ./... +} finally { + docker compose down -v +} +``` + +Expected: PASS. + +- [ ] **Step 5: Commit integration coverage** + +```powershell +git add tests/read_receipts_test.go tests/README.md +@' +test: cover message thread read receipts + +Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> +Copilot-Session: bf00a0ac-e11f-4015-b295-3cdd9b491229 +'@ | git commit -F - +``` + +### Task 8: Verify the complete feature + +**Files:** + +- Review: all files changed since `origin/main` + +**Interfaces:** + +- Consumes: all previous tasks. +- Produces: a clean, verified feature branch. + +- [ ] **Step 1: Run API formatting and tests** + +Run: + +```powershell +Set-Location 'C:\Users\achoa\Work\NdoleStudio\httpsms-read-receipts\api' +go-fumpt -w pkg/entities/message_thread.go pkg/entities/message_thread_test.go pkg/repositories/message_thread_repository.go pkg/repositories/gorm_message_thread_repository.go pkg/repositories/gorm_message_thread_repository_test.go pkg/services/message_thread_service.go pkg/services/message_thread_service_test.go pkg/requests/message_thread_update_request.go pkg/requests/message_thread_update_request_test.go pkg/validators/message_thread_handler_validator.go pkg/validators/message_thread_handler_validator_test.go pkg/responses/message_thead_responses.go pkg/handlers/message_thread_handler.go pkg/listeners/message_thread_listener.go pkg/listeners/read_receipts_test_helpers_test.go pkg/listeners/message_thread_listener_test.go pkg/listeners/websocket_listener.go pkg/listeners/websocket_listener_test.go +go test -vet=off ./pkg/entities ./pkg/repositories ./pkg/requests ./pkg/services ./pkg/validators ./pkg/listeners +go test -vet=off ./pkg/handlers -run '^$' +``` + +Expected: formatting completes and all API tests PASS. + +- [ ] **Step 2: Run complete web validation** + +Run: + +```powershell +Set-Location 'C:\Users\achoa\Work\NdoleStudio\httpsms-read-receipts\web' +pnpm lint +pnpm run generate +``` + +Expected: PASS. + +- [ ] **Step 3: Inspect the final diff** + +Run: + +```powershell +Set-Location 'C:\Users\achoa\Work\NdoleStudio\httpsms-read-receipts' +git --no-pager diff --check origin/main...HEAD +git --no-pager diff --stat origin/main...HEAD +git --no-pager status --short --branch +``` + +Expected: no whitespace errors and no uncommitted implementation changes. + +- [ ] **Step 4: Perform manual acceptance checks against a migrated local database** + +Verify: + +1. Existing rows return `"is_read": true`. +2. A closed thread becomes highlighted after an incoming SMS. +3. A closed thread becomes highlighted after a missed call. +4. Opening the thread removes the highlight and refresh keeps it read. +5. Incoming activity while the thread is open does not leave it unread. +6. Sending and delivery-status changes do not clear an unread thread. +7. Archiving and unarchiving preserve read state. + +- [ ] **Step 5: Commit any formatting-only corrections** + +Skip this step when Step 1 produced no changes. Otherwise: + +```powershell +git add api web +@' +style: format read receipts changes + +Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> +Copilot-Session: bf00a0ac-e11f-4015-b295-3cdd9b491229 +'@ | git commit -F - +``` diff --git a/docs/superpowers/specs/2026-07-18-read-receipts-design.md b/docs/superpowers/specs/2026-07-18-read-receipts-design.md new file mode 100644 index 000000000..6462cbaa5 --- /dev/null +++ b/docs/superpowers/specs/2026-07-18-read-receipts-design.md @@ -0,0 +1,286 @@ +# Message Thread Read Receipts + +- Date: 2026-07-18 +- Status: Approved (design) +- Scope: `api/` Go backend and `web/` Nuxt frontend. Android is unchanged. +- Branch: `feat/read-receipts`, based on `origin/main` + +## Problem + +The message-thread list does not distinguish conversations with unseen inbound +activity from conversations the user has already opened. Users need unread +threads to stand out and become read automatically when opened. + +The change must be backward compatible: every thread that exists when the new +schema is deployed starts as read. + +## Decisions + +- Unread state is stored per message thread. +- Existing threads are read after migration. +- A new incoming SMS or missed call marks its thread unread. +- Outbound messages and later delivery/status updates do not change read state. +- Opening a thread in the web UI marks it read automatically. +- A thread that receives inbound activity while already open remains read. +- There is no manual "Mark as read/unread" UI. +- Unread threads use bold contact/preview text and a subtle primary-tinted + background. +- The existing `PUT /v1/message-threads/{messageThreadID}` endpoint handles the + read update; no new endpoint is added. + +## Design + +### 1. Thread persistence + +Add these fields to `api/pkg/entities/message_thread.go`: + +```go +IsRead bool `json:"is_read" gorm:"not null;default:true" example:"true"` +LastReadAt time.Time `json:"-" gorm:"not null;default:CURRENT_TIMESTAMP"` +``` + +`IsRead` is the public binary state used by API clients and the web UI. +`LastReadAt` is internal ordering metadata used to make concurrent event +processing deterministic. + +GORM `AutoMigrate` adds both columns. The database defaults make all existing +rows read and give them a migration-time read watermark. New-thread creation +sets `IsRead` explicitly according to whether the triggering activity is +inbound. + +Existing-row updates must not use the repository's current full-row `Save` +method. A stale entity loaded by one event handler could otherwise overwrite a +newer read action from the web request. + +### 2. Atomic repository updates + +Add field-scoped repository operations for the two update paths: + +- **Message activity update:** update only `order_timestamp`, + `last_message_id`, `last_message_content`, and `status`. For inbound activity, + conditionally set `is_read = false` only where the stored `last_read_at` is + older than the CloudEvent creation time. +- **User status update:** update only the supplied `is_archived` and `is_read` + fields. Marking read writes `is_read = true` and `last_read_at = now` in the + same database update. + +The message activity update runs its metadata and conditional unread statements +inside one GORM transaction. Both operations remain scoped by `user_id` and +thread ID and use GORM's query builder with context propagation. + +After a user status update, reload the thread for the API response. Do not use +`Save` in either path, because it writes unrelated columns and reintroduces lost +updates. + +The stored timestamp condition prevents this race: + +1. An inbound event is created. +2. The websocket listener notifies the open web page. +3. The web page marks the thread read. +4. The message-thread listener finishes later. + +Because CloudEvent creation time precedes the read request, the late listener +checks the newer database watermark and does not overwrite the read action. If +the message listener commits first, the later read update wins normally. + +### 3. Thread update parameters + +Extend `services.MessageThreadUpdateParams` with: + +```go +MarksUnread bool +EventTimestamp time.Time +``` + +Inbound SMS and missed-call listeners set `MarksUnread = true` and use +`event.Time()` as `EventTimestamp`. Other message events leave `MarksUnread` +false, so sending, scheduling, delivery, failure, and expiry updates preserve +the current read state. + +For an existing thread, the service delegates the field-scoped message update +and optional conditional unread transition to the repository. + +For a newly created thread: + +- inbound SMS or missed call: `IsRead = false`; +- outbound activity: `IsRead = true`. + +### 4. Missed-call thread updates + +`MessageThreadListener` currently handles received SMS events but not +`message.call.missed`. Register the missed-call event and update/create the +corresponding thread with: + +- owner and contact from `MessageCallMissedPayload`; +- message ID and event timestamp; +- received status; +- a concise preview such as `Missed phone call`; +- `MarksUnread = true`. + +This ensures a missed call is visible as thread activity even when no auto-reply +is configured. + +### 5. Existing update endpoint + +Change `requests.MessageThreadUpdate` to use optional fields: + +```go +IsArchived *bool `json:"is_archived,omitempty" example:"true"` +IsRead *bool `json:"is_read,omitempty" example:"true"` +``` + +The validator requires at least one supported field. Pointer fields distinguish +"not supplied" from `false`, preventing a read-only update from unarchiving a +thread and an archive-only update from changing read state. + +`MessageThreadStatusParams` receives the optional fields. `UpdateStatus`: + +- loads the authenticated user's thread; +- builds an update map containing only supplied fields; +- writes `last_read_at = time.Now().UTC()` alongside `is_read = true`; +- supports `IsRead = false` at the API level without adding a web control; +- persists one combined partial update and reloads the updated thread. + +The handler preserves repository error codes so a missing thread returns 404. +Invalid IDs or empty/unsupported update bodies return validation errors. + +### 6. Web store behavior + +Regenerate the API model so `EntitiesMessageThread` includes `is_read` and +`RequestsMessageThreadUpdate` has optional `is_archived` and `is_read`. + +In `web/app/stores/threads.ts`: + +- keep archive updates on the existing endpoint with + `{ is_archived: value }`; +- add `markThreadRead(threadId)` using `{ is_read: true }`; +- replace the matching thread in local state with the successful API response; +- make the operation idempotent and skip the request when the local thread is + already read unless it is called after an inbound realtime event. + +Opening a thread invokes `markThreadRead` automatically as part of the thread +loading flow. Message fetching remains independent: if the read update fails, +messages still display, the thread remains/reloads as unread, and the existing +notification system reports the failure. + +### 7. Realtime behavior + +The message detail page already reloads messages for incoming SMS websocket +events. That reload also marks the currently selected thread read. + +Add `message.call.missed` to: + +- `WebsocketListener` registrations in the API; +- the Pusher bindings on the thread detail page. + +The websocket payload can remain the event ID. The selected thread is marked +read idempotently after any inbound realtime refresh; a different thread's +backend state remains unread. + +### 8. Thread-list presentation + +In `web/app/components/MessageThread.vue`, unread list items receive: + +- bold contact title; +- bold message preview; +- a subtle background using the Vuetify primary theme color at low opacity. + +Read threads retain the current appearance. Active-route styling remains +visible and takes precedence where Vuetify applies it. + +The highlight is applied in the shared thread-list component, so it works in +both the mobile thread page and desktop navigation drawer, including archived +threads. + +### 9. Swagger and generated types + +After changing the entity and request annotations: + +1. Run `swag init --requiredByDefault --parseDependency --parseInternal` in + `api/`. +2. Run `pnpm api:models` in `web/`. + +Generated Swagger files and `web/shared/types/api.ts` are committed with the +implementation. + +## Error Handling + +- Repository and service errors continue to use `stacktrace.Propagate`. +- The update handler returns 404 for a thread that does not belong to the + authenticated user. +- A failed automatic read update is not swallowed and does not block message + display. +- Local UI state changes only from a successful response or a subsequent thread + reload; the UI does not claim a persisted read state after an API failure. +- Realtime callbacks report failures through the existing notification/error + path rather than using empty catches. + +## Testing + +### API + +Add targeted tests for: + +- entity schema defaults and new-thread read-state initialization; +- an inbound SMS making an existing thread unread; +- a missed call creating or updating an unread thread; +- outbound and delivery/status events preserving read state; +- a read action winning when its timestamp is newer than an inbound event; +- concurrent message/read updates changing only their owned columns; +- archive-only updates preserving read state; +- read-only updates preserving archive state; +- missing threads preserving the not-found error code; +- existing-row migration defaults (`is_read = true`). + +Run: + +```bash +cd api +go test ./... +``` + +### Web + +No frontend unit-test runner is configured. Validate the generated types, +Pinia/Vue changes, styles, and production output with: + +```bash +cd web +pnpm lint +pnpm run generate +``` + +### Integration + +Add `tests/read_receipts_test.go` to exercise the feature against the Docker +integration stack: + +- an inbound SMS creates or updates an unread thread; +- marking the thread read through the existing update endpoint persists; +- a missed call makes the thread unread again; +- subsequent outbound activity does not clear that unread state. + +Update `tests/README.md` coverage and run: + +```bash +cd tests +go test -v -timeout 120s -run TestMessageThreadReadReceipts ./... +``` + +Manual acceptance checks: + +1. Existing threads appear read immediately after migration. +2. A new incoming SMS highlights its closed thread. +3. A missed call highlights its closed thread. +4. Opening an unread thread removes the highlight and persists across refresh. +5. Incoming activity in the currently open thread does not leave it unread. +6. Sending or receiving delivery updates does not clear another unread state. +7. Archiving/unarchiving does not change read state. + +## Out of Scope + +- Per-message read receipts. +- Android read/unread UI. +- Unread counts or badges outside the thread list. +- Manual read/unread controls. +- Cross-user/group-conversation receipt tracking. diff --git a/tests/README.md b/tests/README.md index 345d84d1e..45bbb278f 100644 --- a/tests/README.md +++ b/tests/README.md @@ -53,6 +53,7 @@ The API's Firebase SDK is configured (via `FCM_ENDPOINT` env var) to redirect al - [x] **Send SMS E2E** — Full send lifecycle: API → FCM push → emulator responds with SENT/DELIVERED events → message reaches `delivered` status - [x] **Receive SMS E2E** — Phone submits received message to API → message is stored and retrievable via GET endpoint +- [x] **Message thread read receipts E2E** — Incoming SMS and missed calls mark a thread unread, the existing thread update endpoint marks it read, and outbound activity preserves unread state - [x] **Unarchive Thread on Receive E2E** — Archived thread returns to the inbox on inbound message when the phone's `unarchive_thread` setting is enabled, and stays archived when disabled ## Prerequisites diff --git a/tests/read_receipts_test.go b/tests/read_receipts_test.go new file mode 100644 index 000000000..a65e7eb73 --- /dev/null +++ b/tests/read_receipts_test.go @@ -0,0 +1,206 @@ +package tests + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "testing" + "time" + + httpsms "github.com/NdoleStudio/httpsms-go" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +type integrationMessageThread struct { + ID string `json:"id"` + Contact string `json:"contact"` + IsRead bool `json:"is_read"` + LastMessageContent *string `json:"last_message_content"` +} + +func requestJSON( + ctx context.Context, + t *testing.T, + method string, + path string, + apiKey string, + payload any, + expectedStatus int, + output any, +) { + t.Helper() + + var encoded []byte + if payload != nil { + var err error + encoded, err = json.Marshal(payload) + require.NoError(t, err) + } + + deadline := time.Now().Add(20 * time.Second) + for { + request, err := http.NewRequestWithContext(ctx, method, apiBaseURL+path, bytes.NewReader(encoded)) + require.NoError(t, err) + request.Header.Set("x-api-key", apiKey) + request.Header.Set("Content-Type", "application/json") + + response, err := http.DefaultClient.Do(request) + require.NoError(t, err) + + responseBody, err := io.ReadAll(response.Body) + response.Body.Close() + require.NoError(t, err) + + if response.StatusCode == http.StatusUnauthorized && + apiKey != userAPIKey && + time.Now().Before(deadline) { + time.Sleep(500 * time.Millisecond) + continue + } + + require.Equal(t, expectedStatus, response.StatusCode, "response: %s", string(responseBody)) + if output != nil { + require.NoError(t, json.Unmarshal(responseBody, output)) + } + return + } +} + +func fetchMessageThreads(ctx context.Context, t *testing.T, owner string) []integrationMessageThread { + t.Helper() + + var response struct { + Data []integrationMessageThread `json:"data"` + } + path := fmt.Sprintf( + "/v1/message-threads?owner=%s&skip=0&limit=20&is_archived=false", + url.QueryEscape(owner), + ) + requestJSON(ctx, t, http.MethodGet, path, userAPIKey, nil, http.StatusOK, &response) + return response.Data +} + +func waitForMessageThread( + ctx context.Context, + t *testing.T, + owner string, + contact string, + timeout time.Duration, + matches func(integrationMessageThread) bool, +) integrationMessageThread { + t.Helper() + + deadline := time.Now().Add(timeout) + for time.Now().Before(deadline) { + for _, thread := range fetchMessageThreads(ctx, t, owner) { + if thread.Contact == contact && matches(thread) { + return thread + } + } + time.Sleep(500 * time.Millisecond) + } + + t.Fatalf("thread %s -> %s did not reach the expected state within %v", owner, contact, timeout) + return integrationMessageThread{} +} + +func markMessageThreadRead(ctx context.Context, t *testing.T, threadID string) integrationMessageThread { + t.Helper() + + var response struct { + Data integrationMessageThread `json:"data"` + } + requestJSON( + ctx, + t, + http.MethodPut, + "/v1/message-threads/"+threadID, + userAPIKey, + map[string]any{"is_read": true}, + http.StatusOK, + &response, + ) + return response.Data +} + +func TestMessageThreadReadReceipts(t *testing.T) { + ctx := context.Background() + phone := setupPhone(ctx, t, 60) + contact := randomPhoneNumber() + + requestJSON( + ctx, + t, + http.MethodPost, + "/v1/messages/receive", + phone.PhoneAPIKey, + map[string]any{ + "from": contact, + "to": phone.PhoneNumber, + "content": "Unread inbound message", + "encrypted": false, + "sim": "SIM1", + "timestamp": time.Now().UTC().Format(time.RFC3339Nano), + }, + http.StatusOK, + nil, + ) + + thread := waitForMessageThread(ctx, t, phone.PhoneNumber, contact, 20*time.Second, func(thread integrationMessageThread) bool { + return !thread.IsRead + }) + assert.False(t, thread.IsRead) + + updated := markMessageThreadRead(ctx, t, thread.ID) + assert.True(t, updated.IsRead) + assert.Equal(t, contact, updated.Contact) + require.NotNil(t, updated.LastMessageContent) + assert.Equal(t, "Unread inbound message", *updated.LastMessageContent) + waitForMessageThread(ctx, t, phone.PhoneNumber, contact, 10*time.Second, func(thread integrationMessageThread) bool { + return thread.IsRead + }) + + requestJSON( + ctx, + t, + http.MethodPost, + "/v1/messages/calls/missed", + phone.PhoneAPIKey, + map[string]any{ + "from": contact, + "to": phone.PhoneNumber, + "sim": "SIM1", + "timestamp": time.Now().UTC().Format(time.RFC3339Nano), + }, + http.StatusOK, + nil, + ) + + thread = waitForMessageThread(ctx, t, phone.PhoneNumber, contact, 20*time.Second, func(thread integrationMessageThread) bool { + return !thread.IsRead && + thread.LastMessageContent != nil && + *thread.LastMessageContent == "Missed phone call" + }) + assert.False(t, thread.IsRead) + + outboundContent := "Outbound activity preserves unread" + client := newAPIClient() + _, response, err := client.Messages.Send(ctx, &httpsms.MessageSendParams{ + From: phone.PhoneNumber, + To: contact, + Content: outboundContent, + }) + require.NoError(t, err) + require.Equal(t, http.StatusOK, response.HTTPResponse.StatusCode) + + thread = waitForMessageThread(ctx, t, phone.PhoneNumber, contact, 20*time.Second, func(thread integrationMessageThread) bool { + return thread.LastMessageContent != nil && + *thread.LastMessageContent == outboundContent + }) + assert.False(t, thread.IsRead, "outbound activity must not clear unread state") +} diff --git a/web/app/components/MessageThread.vue b/web/app/components/MessageThread.vue index 029599180..2a9834317 100644 --- a/web/app/components/MessageThread.vue +++ b/web/app/components/MessageThread.vue @@ -99,7 +99,11 @@ function onInstallApp() { :active="threadsStore.threadId === thread.id" > - {{ + {{ formatPhoneNumber(thread.contact) }} {{ thread.last_message_content }} diff --git a/web/app/pages/threads/[id]/index.vue b/web/app/pages/threads/[id]/index.vue index 1fd11eae1..73632a64c 100644 --- a/web/app/pages/threads/[id]/index.vue +++ b/web/app/pages/threads/[id]/index.vue @@ -112,9 +112,19 @@ function scrollToElement() { hideMessages.value = false } -function loadMessages(hide = true) { +async function markCurrentThreadRead(force = false) { + const threadId = route.params.id as string + try { + await threadsStore.markThreadRead(threadId, force) + } catch (error) { + console.error(error) + } +} + +function loadMessages(hide = true, markRead = true) { loadingMessages.value = true const threadId = route.params.id as string + if (markRead) void markCurrentThreadRead() threadsStore .loadThreadMessages(threadId) .then((response: EntitiesMessage[]) => { @@ -232,7 +242,16 @@ onMounted(async () => { if (!loadingMessages.value) loadMessages(false) }) webhookChannel.bind('message.phone.received', () => { - if (!loadingMessages.value) loadMessages(false) + if (!loadingMessages.value) { + void markCurrentThreadRead(true) + loadMessages(false, false) + } + }) + webhookChannel.bind('message.call.missed', () => { + if (!loadingMessages.value) { + void markCurrentThreadRead(true) + loadMessages(false, false) + } }) }) diff --git a/web/app/stores/threads.ts b/web/app/stores/threads.ts index aef2b2947..7e3002624 100644 --- a/web/app/stores/threads.ts +++ b/web/app/stores/threads.ts @@ -24,6 +24,13 @@ export const useThreadsStore = defineStore('threads', () => { return threads.value.find((x) => x.id === id) !== undefined } + function replaceThread(updatedThread: EntitiesMessageThread) { + const index = threads.value.findIndex( + (thread) => thread.id === updatedThread.id, + ) + if (index !== -1) threads.value[index] = updatedThread + } + async function loadThreads() { const phonesStore = usePhonesStore() if (phonesStore.owner === null && phonesStore.phones.length === 0) { @@ -93,6 +100,38 @@ export const useThreadsStore = defineStore('threads', () => { }) } + async function markThreadRead(threadId: string, force = false) { + const thread = threads.value.find((item) => item.id === threadId) + if (!thread) throw new Error(`Cannot find thread with id ${threadId}`) + if (!force && thread.is_read) return + + try { + const response = await apiFetch<{ data: EntitiesMessageThread }>( + `/v1/message-threads/${threadId}`, + { + method: 'PUT', + body: { is_read: true }, + }, + ) + replaceThread(response.data) + } catch (error) { + notificationsStore.addNotification({ + message: 'The message thread could not be marked as read', + type: 'error', + }) + try { + await loadThreads() + } catch (reloadError) { + throw new AggregateError( + [error, reloadError], + 'Could not mark the message thread as read or reload threads', + { cause: reloadError }, + ) + } + throw error + } + } + async function deleteThread(id: string) { await apiFetch(`/v1/message-threads/${id}`, { method: 'DELETE' }) threadId.value = null @@ -122,6 +161,7 @@ export const useThreadsStore = defineStore('threads', () => { setThreadId, toggleArchive, updateThread, + markThreadRead, deleteThread, resetState, } diff --git a/web/shared/types/api.ts b/web/shared/types/api.ts index 3f19b8534..27e2db111 100644 --- a/web/shared/types/api.ts +++ b/web/shared/types/api.ts @@ -205,6 +205,8 @@ export interface EntitiesMessageThread { id: string; /** @example false */ is_archived: boolean; + /** @example true */ + is_read: boolean; /** @example "This is a sample message content" */ last_message_content: string; /** @example "32343a19-da5e-4b1b-a767-3298a73703ca" */ @@ -484,7 +486,9 @@ export interface RequestsMessageSendScheduleWindow { export interface RequestsMessageThreadUpdate { /** @example true */ - is_archived: boolean; + is_archived?: boolean; + /** @example true */ + is_read?: boolean; } export interface RequestsPhoneAPIKeyStoreRequest { @@ -684,6 +688,14 @@ export interface ResponsesMessageSendSchedulesResponse { status: string; } +export interface ResponsesMessageThreadResponse { + data: EntitiesMessageThread; + /** @example "Request handled successfully" */ + message: string; + /** @example "success" */ + status: string; +} + export interface ResponsesMessageThreadsResponse { data: EntitiesMessageThread[]; /** @example "Request handled successfully" */ From 8b37ec7ef80e301d5a4aa29e89f5b40a8d6deb95 Mon Sep 17 00:00:00 2001 From: Acho Arnold Ewin Date: Sun, 19 Jul 2026 15:04:22 +0300 Subject: [PATCH 02/88] docs: add email number formatting design Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: ddea6ddd-8c2a-4505-a002-64e92b067846 --- ...26-07-19-email-number-formatting-design.md | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 docs/superpowers/specs/2026-07-19-email-number-formatting-design.md diff --git a/docs/superpowers/specs/2026-07-19-email-number-formatting-design.md b/docs/superpowers/specs/2026-07-19-email-number-formatting-design.md new file mode 100644 index 000000000..6e715b1d2 --- /dev/null +++ b/docs/superpowers/specs/2026-07-19-email-number-formatting-design.md @@ -0,0 +1,30 @@ +# Email Number Formatting Design + +## Goal + +Format integer quantities in generated emails with comma thousands separators. For example, `20000` becomes `20,000`. + +## Scope + +Apply formatting to message counts and subscription limits in usage-limit emails: + +- sent messages +- received messages +- total messages +- plan message limits + +Do not change dates, percentages, HTTP status codes, phone numbers, years, or numeric event payload values. + +## Design + +Add a private integer-formatting helper to the shared email `factory`. The helper will produce deterministic comma-grouped decimal output without adding a dependency. + +Use the helper in `hermes_user_email_factory.go` anywhere a message count or subscription limit is rendered. Because Hermes generates both HTML and plain text from the same content model, both email formats will receive identical formatting. + +## Error Handling + +Formatting is a pure operation and introduces no new error path. Existing email generation errors continue to propagate unchanged. + +## Testing + +Add focused tests for integer grouping, including values below and above one thousand. Update usage-limit email tests to assert formatted quantities in both generated plain text and HTML. From 9bac0f7a22a8f8ec177005c777e07aa07303cc1c Mon Sep 17 00:00:00 2001 From: Acho Arnold Ewin Date: Sun, 19 Jul 2026 15:12:43 +0300 Subject: [PATCH 03/88] fix(api): format email quantities Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: ddea6ddd-8c2a-4505-a002-64e92b067846 --- api/pkg/emails/factory.go | 10 +++++++++ api/pkg/emails/hermes_user_email_factory.go | 6 ++--- .../emails/hermes_user_email_factory_test.go | 22 ++++++++++--------- 3 files changed, 25 insertions(+), 13 deletions(-) diff --git a/api/pkg/emails/factory.go b/api/pkg/emails/factory.go index 992f48646..63d85f5e2 100644 --- a/api/pkg/emails/factory.go +++ b/api/pkg/emails/factory.go @@ -3,6 +3,7 @@ package emails import ( "fmt" "net/http" + "strconv" "github.com/nyaruka/phonenumbers" ) @@ -21,6 +22,15 @@ func (factory *factory) formatBool(value bool) string { return "No" } +func (factory *factory) formatQuantity(value uint) string { + formatted := strconv.FormatUint(uint64(value), 10) + for index := len(formatted) - 3; index > 0; index -= 3 { + formatted = formatted[:index] + "," + formatted[index:] + } + + return formatted +} + func (factory *factory) formatHTTPResponseCode(code *int) string { responseCode := "-" if code != nil { diff --git a/api/pkg/emails/hermes_user_email_factory.go b/api/pkg/emails/hermes_user_email_factory.go index cae4a3b9a..174b0f420 100644 --- a/api/pkg/emails/hermes_user_email_factory.go +++ b/api/pkg/emails/hermes_user_email_factory.go @@ -73,8 +73,8 @@ func (factory *hermesUserEmailFactory) UsageLimitExceeded(user *entities.User, u email := hermes.Email{ Body: hermes.Body{ Intros: []string{ - fmt.Sprintf("You've reached your limit of %d messages on the %s plan, so new messages will not be processed until your usage resets.", user.SubscriptionName.Limit(), user.SubscriptionName), - fmt.Sprintf("Between %s and %s you sent %d messages and received %d, for a total of %d.", formatBillingDate(usage.StartTimestamp, user.Location()), formatBillingDate(usage.EndTimestamp, user.Location()), usage.SentMessages, usage.ReceivedMessages, usage.TotalMessages()), + fmt.Sprintf("You've reached your limit of %s messages on the %s plan, so new messages will not be processed until your usage resets.", factory.formatQuantity(user.SubscriptionName.Limit()), user.SubscriptionName), + fmt.Sprintf("Between %s and %s you sent %s messages and received %s, for a total of %s.", formatBillingDate(usage.StartTimestamp, user.Location()), formatBillingDate(usage.EndTimestamp, user.Location()), factory.formatQuantity(usage.SentMessages), factory.formatQuantity(usage.ReceivedMessages), factory.formatQuantity(usage.TotalMessages())), }, Actions: []hermes.Action{ { @@ -120,7 +120,7 @@ func (factory *hermesUserEmailFactory) UsageLimitAlert(user *entities.User, usag Body: hermes.Body{ Intros: []string{ fmt.Sprintf("This is a friendly heads-up that you've used %d%% of your monthly SMS limit on the %s plan.", percent, user.SubscriptionName), - fmt.Sprintf("Between %s and %s you sent %d messages and received %d, for a total of %d out of your %d message limit.", formatBillingDate(usage.StartTimestamp, user.Location()), formatBillingDate(usage.EndTimestamp, user.Location()), usage.SentMessages, usage.ReceivedMessages, usage.TotalMessages(), user.SubscriptionName.Limit()), + fmt.Sprintf("Between %s and %s you sent %s messages and received %s, for a total of %s out of your %s message limit.", formatBillingDate(usage.StartTimestamp, user.Location()), formatBillingDate(usage.EndTimestamp, user.Location()), factory.formatQuantity(usage.SentMessages), factory.formatQuantity(usage.ReceivedMessages), factory.formatQuantity(usage.TotalMessages()), factory.formatQuantity(user.SubscriptionName.Limit())), }, Actions: []hermes.Action{ { diff --git a/api/pkg/emails/hermes_user_email_factory_test.go b/api/pkg/emails/hermes_user_email_factory_test.go index df322e926..e10e3f487 100644 --- a/api/pkg/emails/hermes_user_email_factory_test.go +++ b/api/pkg/emails/hermes_user_email_factory_test.go @@ -51,10 +51,11 @@ func TestUsageLimitExceeded_IncludesBreakdownAndBillingPeriod(t *testing.T) { assert.NoError(t, err) assert.Equal(t, "name@email.com", email.ToEmail) assert.Equal(t, "⚠️ You have exceeded your plan limit", email.Subject) - assert.Contains(t, email.Text, "limit of 5000 messages") + assert.Contains(t, email.Text, "limit of 5,000 messages") assert.Contains(t, email.Text, "Between 19 June 2026 and 18 July 2026") - assert.Contains(t, email.Text, "you sent 3000 messages and received 2000") - assert.Contains(t, email.Text, "for a total of 5000") + assert.Contains(t, email.Text, "you sent 3,000 messages and received 2,000") + assert.Contains(t, email.Text, "for a total of 5,000") + assert.Contains(t, email.HTML, "you sent 3,000 messages and received 2,000") } func TestUsageLimitAlert_IncludesPercentBreakdownAndLimit(t *testing.T) { @@ -62,11 +63,11 @@ func TestUsageLimitAlert_IncludesPercentBreakdownAndLimit(t *testing.T) { user := &entities.User{ Email: "name@email.com", Timezone: "UTC", - SubscriptionName: entities.SubscriptionNameProMonthly, + SubscriptionName: entities.SubscriptionName20KMonthly, } usage := &entities.BillingUsage{ - SentMessages: 2500, - ReceivedMessages: 1500, + SentMessages: 11_357, + ReceivedMessages: 6_643, StartTimestamp: time.Date(2026, 6, 19, 0, 0, 0, 0, time.UTC), EndTimestamp: time.Date(2026, 7, 18, 23, 59, 59, 0, time.UTC), } @@ -75,9 +76,10 @@ func TestUsageLimitAlert_IncludesPercentBreakdownAndLimit(t *testing.T) { assert.NoError(t, err) assert.Equal(t, "name@email.com", email.ToEmail) - assert.Equal(t, "⚠️ 80% Usage Limit Alert", email.Subject) - assert.Contains(t, email.Text, "used 80% of your monthly SMS limit") + assert.Equal(t, "⚠️ 90% Usage Limit Alert", email.Subject) + assert.Contains(t, email.Text, "used 90% of your monthly SMS limit") assert.Contains(t, email.Text, "Between 19 June 2026 and 18 July 2026") - assert.Contains(t, email.Text, "you sent 2500 messages and received 1500") - assert.Contains(t, email.Text, "for a total of 4000 out of your 5000 message limit") + assert.Contains(t, email.Text, "you sent 11,357 messages and received 6,643") + assert.Contains(t, email.Text, "for a total of 18,000 out of your 20,000 message limit") + assert.Contains(t, email.HTML, "for a total of 18,000 out of your 20,000 message limit") } From ef6225beeaeae8875209f1dc260bf64dd21d9a41 Mon Sep 17 00:00:00 2001 From: Acho Arnold Ewin Date: Sun, 19 Jul 2026 15:16:10 +0300 Subject: [PATCH 04/88] build(api): use NdoleStudio stacktrace fork Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 10c01eca-aeb5-4fc2-a546-02cee4261d2b --- api/go.mod | 2 +- api/go.sum | 4 ++-- api/pkg/cache/memory_cache.go | 2 +- api/pkg/cache/redis_cache.go | 2 +- api/pkg/di/container.go | 2 +- api/pkg/emails/hermes_notification_email_factory.go | 2 +- api/pkg/emails/hermes_user_email_factory.go | 2 +- api/pkg/emails/smtp_mailer_service.go | 2 +- api/pkg/handlers/attachment_handler.go | 2 +- api/pkg/handlers/billing_handler.go | 2 +- api/pkg/handlers/bulk_message_handler.go | 2 +- api/pkg/handlers/discord_handler.go | 2 +- api/pkg/handlers/events_handler.go | 2 +- api/pkg/handlers/heartbeat_handler.go | 2 +- api/pkg/handlers/integration_3cx_handler.go | 2 +- api/pkg/handlers/lemonsqueezy_handler.go | 2 +- api/pkg/handlers/message_handler.go | 2 +- api/pkg/handlers/message_send_schedule_handler.go | 2 +- api/pkg/handlers/message_thread_handler.go | 2 +- api/pkg/handlers/message_thread_handler_test.go | 2 +- api/pkg/handlers/phone_api_key_handler.go | 2 +- api/pkg/handlers/phone_handler.go | 2 +- api/pkg/handlers/user_handler.go | 2 +- api/pkg/handlers/webhook_handler.go | 2 +- api/pkg/listeners/billing_listener.go | 2 +- api/pkg/listeners/discord_listener.go | 2 +- api/pkg/listeners/email_notification_listener.go | 2 +- api/pkg/listeners/heartbeat_listener.go | 2 +- api/pkg/listeners/integration_3cx_listener.go | 2 +- api/pkg/listeners/marketing_listener.go | 2 +- api/pkg/listeners/message_listener.go | 2 +- api/pkg/listeners/message_send_schedule_listener.go | 2 +- api/pkg/listeners/message_thread_listener.go | 2 +- api/pkg/listeners/phone_api_key_listener.go | 2 +- api/pkg/listeners/phone_listener.go | 2 +- api/pkg/listeners/phone_notification_listener.go | 2 +- api/pkg/listeners/user_listener.go | 2 +- api/pkg/listeners/webhook_listener.go | 2 +- api/pkg/listeners/websocket_listener.go | 2 +- api/pkg/middlewares/api_key_auth_middleware.go | 2 +- api/pkg/middlewares/bearer_api_key_auth_middleware.go | 2 +- api/pkg/middlewares/bearer_auth_middleware.go | 2 +- api/pkg/middlewares/http_request_logger_middleware.go | 2 +- api/pkg/middlewares/phone_api_key_auth_middleware.go | 2 +- .../google_cloud_storage_attachment_repository.go | 2 +- api/pkg/repositories/gorm_billing_usage_repository.go | 2 +- api/pkg/repositories/gorm_discord_repository.go | 2 +- api/pkg/repositories/gorm_heartbeat_monitor_repository.go | 2 +- api/pkg/repositories/gorm_heartbeat_repository.go | 2 +- api/pkg/repositories/gorm_integration_3cx_repository.go | 2 +- api/pkg/repositories/gorm_message_repository.go | 2 +- api/pkg/repositories/gorm_message_send_schedule_repository.go | 2 +- api/pkg/repositories/gorm_message_thread_repository.go | 2 +- api/pkg/repositories/gorm_phone_api_key_repository.go | 2 +- api/pkg/repositories/gorm_phone_notification_repository.go | 2 +- api/pkg/repositories/gorm_phone_repository.go | 2 +- api/pkg/repositories/gorm_user_repository.go | 2 +- api/pkg/repositories/gorm_webhook_repository.go | 2 +- api/pkg/repositories/memory_attachment_repository.go | 2 +- api/pkg/repositories/mongo_heartbeat_monitor_repository.go | 2 +- api/pkg/repositories/mongo_heartbeat_repository.go | 2 +- api/pkg/repositories/mongodb.go | 2 +- api/pkg/repositories/repository.go | 2 +- api/pkg/services/billing_service.go | 2 +- api/pkg/services/discord_service.go | 2 +- api/pkg/services/email_notification_service.go | 2 +- api/pkg/services/emulator_fcm_client.go | 2 +- api/pkg/services/emulator_push_queue.go | 2 +- api/pkg/services/entitlement_service.go | 2 +- api/pkg/services/event_dispatcher_service.go | 2 +- api/pkg/services/google_cloud_push_queue_service.go | 2 +- api/pkg/services/heartbeat_service.go | 2 +- api/pkg/services/integration_3cx_service.go | 2 +- api/pkg/services/lemonsqueezy_service.go | 2 +- api/pkg/services/marketting_service.go | 2 +- api/pkg/services/message_send_schedule_service.go | 2 +- api/pkg/services/message_service.go | 2 +- api/pkg/services/message_thread_service.go | 2 +- api/pkg/services/message_thread_service_test.go | 2 +- api/pkg/services/phone_api_key_service.go | 2 +- api/pkg/services/phone_notification_service.go | 2 +- api/pkg/services/phone_service.go | 2 +- api/pkg/services/service.go | 2 +- api/pkg/services/user_service.go | 2 +- api/pkg/services/webhook_service.go | 2 +- api/pkg/telemetry/gorm_logger.go | 2 +- api/pkg/validators/bulk_message_handler_validator.go | 2 +- api/pkg/validators/discord_handler_validator.go | 2 +- api/pkg/validators/message_handler_validator.go | 2 +- api/pkg/validators/turnstile_token_validator.go | 2 +- api/pkg/validators/user_handler_validator.go | 2 +- api/pkg/validators/webhook_handler_validator.go | 2 +- 92 files changed, 93 insertions(+), 93 deletions(-) diff --git a/api/go.mod b/api/go.mod index 565e15d4a..649489f65 100644 --- a/api/go.mod +++ b/api/go.mod @@ -12,6 +12,7 @@ require ( github.com/NdoleStudio/go-otelroundtripper v0.0.15 github.com/NdoleStudio/lemonsqueezy-go v1.3.1 github.com/NdoleStudio/plunk-go v0.0.2 + github.com/NdoleStudio/stacktrace v1.0.0 github.com/avast/retry-go/v5 v5.0.0 github.com/axiomhq/axiom-go v0.32.0 github.com/carlmjohnson/requests v0.25.1 @@ -35,7 +36,6 @@ require ( github.com/jszwec/csvutil v1.10.0 github.com/lib/pq v1.12.3 github.com/nyaruka/phonenumbers v1.8.0 - github.com/palantir/stacktrace v0.0.0-20161112013806-78658fd2d177 github.com/patrickmn/go-cache v2.1.0+incompatible github.com/pkg/errors v0.9.1 github.com/pusher/pusher-http-go/v5 v5.1.1 diff --git a/api/go.sum b/api/go.sum index dc7298013..264094f1a 100644 --- a/api/go.sum +++ b/api/go.sum @@ -60,6 +60,8 @@ github.com/NdoleStudio/lemonsqueezy-go v1.3.1 h1:lMUVgdAx2onbOUJIVPR05xAANYuCMXB github.com/NdoleStudio/lemonsqueezy-go v1.3.1/go.mod h1:xKRsRX1jSI6mLrVXyWh2sF/1isxTioZrSjWy6HpA3xQ= github.com/NdoleStudio/plunk-go v0.0.2 h1:afPW7MHK4Z3rsybpJBnmTmxKCLKF1M7sPI+BNGPf35A= github.com/NdoleStudio/plunk-go v0.0.2/go.mod h1:pqG3zKhpn/A2bL1K+WsWzvfTpOeSkYgXhNk5H65uEc8= +github.com/NdoleStudio/stacktrace v1.0.0 h1:YIDg0twM2BXRKIFjorX7v2dC0l8z4VEITurI+MNqvVQ= +github.com/NdoleStudio/stacktrace v1.0.0/go.mod h1:WOBcbXShYu+v8JtjFEM+rRV9GJW50m+dwIvp4sLuVTg= github.com/PuerkitoBio/goquery v1.12.0 h1:pAcL4g3WRXekcB9AU/y1mbKez2dbY2AajVhtkO8RIBo= github.com/PuerkitoBio/goquery v1.12.0/go.mod h1:802ej+gV2y7bbIhOIoPY5sT183ZW0YFofScC4q/hIpQ= github.com/andybalholm/brotli v1.2.1 h1:R+f5xP285VArJDRgowrfb9DqL18yVK0gKAW/F+eTWro= @@ -278,8 +280,6 @@ github.com/olekukonko/ll v0.1.8 h1:ysHCJRGHYKzmBSdz9w5AySztx7lG8SQY+naTGYUbsz8= github.com/olekukonko/ll v0.1.8/go.mod h1:RPRC6UcscfFZgjo1nulkfMH5IM0QAYim0LfnMvUuozw= github.com/olekukonko/tablewriter v1.1.4 h1:ORUMI3dXbMnRlRggJX3+q7OzQFDdvgbN9nVWj1drm6I= github.com/olekukonko/tablewriter v1.1.4/go.mod h1:+kedxuyTtgoZLwif3P1Em4hARJs+mVnzKxmsCL/C5RY= -github.com/palantir/stacktrace v0.0.0-20161112013806-78658fd2d177 h1:nRlQD0u1871kaznCnn1EvYiMbum36v7hw1DLPEjds4o= -github.com/palantir/stacktrace v0.0.0-20161112013806-78658fd2d177/go.mod h1:ao5zGxj8Z4x60IOVYZUbDSmt3R8Ddo080vEgPosHpak= github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= github.com/paulmach/orb v0.13.0 h1:r7n7mQGGF+cj/CbcivEj9J3HGK+XR+yXnvzRdq9saIw= diff --git a/api/pkg/cache/memory_cache.go b/api/pkg/cache/memory_cache.go index 4500640c0..e749ca3e3 100644 --- a/api/pkg/cache/memory_cache.go +++ b/api/pkg/cache/memory_cache.go @@ -5,7 +5,7 @@ import ( "time" "github.com/NdoleStudio/httpsms/pkg/telemetry" - "github.com/palantir/stacktrace" + "github.com/NdoleStudio/stacktrace" ttlCache "github.com/patrickmn/go-cache" ) diff --git a/api/pkg/cache/redis_cache.go b/api/pkg/cache/redis_cache.go index 4bbdebcee..990f09bcd 100644 --- a/api/pkg/cache/redis_cache.go +++ b/api/pkg/cache/redis_cache.go @@ -6,7 +6,7 @@ import ( "time" "github.com/NdoleStudio/httpsms/pkg/telemetry" - "github.com/palantir/stacktrace" + "github.com/NdoleStudio/stacktrace" "github.com/redis/go-redis/v9" ) diff --git a/api/pkg/di/container.go b/api/pkg/di/container.go index 62fee7624..6d933ae86 100644 --- a/api/pkg/di/container.go +++ b/api/pkg/di/container.go @@ -67,10 +67,10 @@ import ( "github.com/NdoleStudio/httpsms/pkg/listeners" "github.com/NdoleStudio/httpsms/pkg/repositories" "github.com/NdoleStudio/httpsms/pkg/services" + "github.com/NdoleStudio/stacktrace" swagger "github.com/gofiber/contrib/v3/swaggo" "github.com/gofiber/fiber/v3" fiberLogger "github.com/gofiber/fiber/v3/middleware/logger" - "github.com/palantir/stacktrace" ttlCache "github.com/patrickmn/go-cache" "gorm.io/gorm" diff --git a/api/pkg/emails/hermes_notification_email_factory.go b/api/pkg/emails/hermes_notification_email_factory.go index 2229a502a..4c0440646 100644 --- a/api/pkg/emails/hermes_notification_email_factory.go +++ b/api/pkg/emails/hermes_notification_email_factory.go @@ -8,8 +8,8 @@ import ( "github.com/NdoleStudio/httpsms/pkg/events" "github.com/NdoleStudio/httpsms/pkg/entities" + "github.com/NdoleStudio/stacktrace" "github.com/go-hermes/hermes/v2" - "github.com/palantir/stacktrace" ) type hermesNotificationEmailFactory struct { diff --git a/api/pkg/emails/hermes_user_email_factory.go b/api/pkg/emails/hermes_user_email_factory.go index 174b0f420..8070a78e7 100644 --- a/api/pkg/emails/hermes_user_email_factory.go +++ b/api/pkg/emails/hermes_user_email_factory.go @@ -5,8 +5,8 @@ import ( "time" "github.com/NdoleStudio/httpsms/pkg/entities" + "github.com/NdoleStudio/stacktrace" "github.com/go-hermes/hermes/v2" - "github.com/palantir/stacktrace" ) type hermesUserEmailFactory struct { diff --git a/api/pkg/emails/smtp_mailer_service.go b/api/pkg/emails/smtp_mailer_service.go index bebea4a0c..5f57d1daf 100644 --- a/api/pkg/emails/smtp_mailer_service.go +++ b/api/pkg/emails/smtp_mailer_service.go @@ -6,8 +6,8 @@ import ( "net/smtp" "github.com/NdoleStudio/httpsms/pkg/telemetry" + "github.com/NdoleStudio/stacktrace" mail "github.com/jordan-wright/email" - "github.com/palantir/stacktrace" ) // SMTPConfig is the config for setting up the smtpMailer diff --git a/api/pkg/handlers/attachment_handler.go b/api/pkg/handlers/attachment_handler.go index 0b058f505..e73f12530 100644 --- a/api/pkg/handlers/attachment_handler.go +++ b/api/pkg/handlers/attachment_handler.go @@ -6,8 +6,8 @@ import ( "github.com/NdoleStudio/httpsms/pkg/repositories" "github.com/NdoleStudio/httpsms/pkg/telemetry" + "github.com/NdoleStudio/stacktrace" "github.com/gofiber/fiber/v3" - "github.com/palantir/stacktrace" ) // AttachmentHandler handles attachment download requests diff --git a/api/pkg/handlers/billing_handler.go b/api/pkg/handlers/billing_handler.go index 33f0b2f0d..a33ad2312 100644 --- a/api/pkg/handlers/billing_handler.go +++ b/api/pkg/handlers/billing_handler.go @@ -7,9 +7,9 @@ import ( "github.com/NdoleStudio/httpsms/pkg/services" "github.com/NdoleStudio/httpsms/pkg/telemetry" "github.com/NdoleStudio/httpsms/pkg/validators" + "github.com/NdoleStudio/stacktrace" "github.com/davecgh/go-spew/spew" "github.com/gofiber/fiber/v3" - "github.com/palantir/stacktrace" ) // BillingHandler handles billing http requests. diff --git a/api/pkg/handlers/bulk_message_handler.go b/api/pkg/handlers/bulk_message_handler.go index 01e451469..28e0d0ad4 100644 --- a/api/pkg/handlers/bulk_message_handler.go +++ b/api/pkg/handlers/bulk_message_handler.go @@ -12,9 +12,9 @@ import ( "github.com/NdoleStudio/httpsms/pkg/services" "github.com/NdoleStudio/httpsms/pkg/telemetry" "github.com/NdoleStudio/httpsms/pkg/validators" + "github.com/NdoleStudio/stacktrace" "github.com/davecgh/go-spew/spew" "github.com/gofiber/fiber/v3" - "github.com/palantir/stacktrace" ) // BulkMessageHandler handles bulk SMS http requests diff --git a/api/pkg/handlers/discord_handler.go b/api/pkg/handlers/discord_handler.go index 29da67a07..582cdb325 100644 --- a/api/pkg/handlers/discord_handler.go +++ b/api/pkg/handlers/discord_handler.go @@ -16,9 +16,9 @@ import ( "github.com/NdoleStudio/httpsms/pkg/services" "github.com/NdoleStudio/httpsms/pkg/telemetry" "github.com/NdoleStudio/httpsms/pkg/validators" + "github.com/NdoleStudio/stacktrace" "github.com/davecgh/go-spew/spew" "github.com/gofiber/fiber/v3" - "github.com/palantir/stacktrace" ) // DiscordHandler handles discord events diff --git a/api/pkg/handlers/events_handler.go b/api/pkg/handlers/events_handler.go index b2586ed21..5d4fc121b 100644 --- a/api/pkg/handlers/events_handler.go +++ b/api/pkg/handlers/events_handler.go @@ -7,9 +7,9 @@ import ( "github.com/NdoleStudio/httpsms/pkg/services" "github.com/NdoleStudio/httpsms/pkg/telemetry" + "github.com/NdoleStudio/stacktrace" "github.com/davecgh/go-spew/spew" "github.com/gofiber/fiber/v3" - "github.com/palantir/stacktrace" ) // EventsHandler handles heartbeat http requests. diff --git a/api/pkg/handlers/heartbeat_handler.go b/api/pkg/handlers/heartbeat_handler.go index eb9dec405..663d16af3 100644 --- a/api/pkg/handlers/heartbeat_handler.go +++ b/api/pkg/handlers/heartbeat_handler.go @@ -10,9 +10,9 @@ import ( "github.com/NdoleStudio/httpsms/pkg/services" "github.com/NdoleStudio/httpsms/pkg/telemetry" "github.com/NdoleStudio/httpsms/pkg/validators" + "github.com/NdoleStudio/stacktrace" "github.com/davecgh/go-spew/spew" "github.com/gofiber/fiber/v3" - "github.com/palantir/stacktrace" ) // HeartbeatHandler handles heartbeat http requests. diff --git a/api/pkg/handlers/integration_3cx_handler.go b/api/pkg/handlers/integration_3cx_handler.go index e53213f42..db06e64df 100644 --- a/api/pkg/handlers/integration_3cx_handler.go +++ b/api/pkg/handlers/integration_3cx_handler.go @@ -8,8 +8,8 @@ import ( "github.com/NdoleStudio/httpsms/pkg/services" "github.com/NdoleStudio/httpsms/pkg/telemetry" + "github.com/NdoleStudio/stacktrace" "github.com/gofiber/fiber/v3" - "github.com/palantir/stacktrace" ) // Integration3CXHandler handles 3CX events diff --git a/api/pkg/handlers/lemonsqueezy_handler.go b/api/pkg/handlers/lemonsqueezy_handler.go index ce3ac5aac..1b18d8baa 100644 --- a/api/pkg/handlers/lemonsqueezy_handler.go +++ b/api/pkg/handlers/lemonsqueezy_handler.go @@ -9,9 +9,9 @@ import ( "github.com/NdoleStudio/httpsms/pkg/telemetry" "github.com/NdoleStudio/httpsms/pkg/validators" lemonsqueezy "github.com/NdoleStudio/lemonsqueezy-go" + "github.com/NdoleStudio/stacktrace" "github.com/davecgh/go-spew/spew" "github.com/gofiber/fiber/v3" - "github.com/palantir/stacktrace" ) // LemonsqueezyHandler handles lemonsqueezy events diff --git a/api/pkg/handlers/message_handler.go b/api/pkg/handlers/message_handler.go index 542fc7d04..14a4e95e5 100644 --- a/api/pkg/handlers/message_handler.go +++ b/api/pkg/handlers/message_handler.go @@ -16,9 +16,9 @@ import ( "github.com/NdoleStudio/httpsms/pkg/services" "github.com/NdoleStudio/httpsms/pkg/telemetry" "github.com/NdoleStudio/httpsms/pkg/validators" + "github.com/NdoleStudio/stacktrace" "github.com/davecgh/go-spew/spew" "github.com/gofiber/fiber/v3" - "github.com/palantir/stacktrace" ) // MessageHandler handles message http requests. diff --git a/api/pkg/handlers/message_send_schedule_handler.go b/api/pkg/handlers/message_send_schedule_handler.go index 90d65cca9..93fac9480 100644 --- a/api/pkg/handlers/message_send_schedule_handler.go +++ b/api/pkg/handlers/message_send_schedule_handler.go @@ -9,10 +9,10 @@ import ( "github.com/NdoleStudio/httpsms/pkg/services" "github.com/NdoleStudio/httpsms/pkg/telemetry" "github.com/NdoleStudio/httpsms/pkg/validators" + "github.com/NdoleStudio/stacktrace" "github.com/davecgh/go-spew/spew" "github.com/gofiber/fiber/v3" "github.com/google/uuid" - "github.com/palantir/stacktrace" ) // MessageSendScheduleHandler handles HTTP requests for message send schedules. diff --git a/api/pkg/handlers/message_thread_handler.go b/api/pkg/handlers/message_thread_handler.go index 9520b6634..996ca69be 100644 --- a/api/pkg/handlers/message_thread_handler.go +++ b/api/pkg/handlers/message_thread_handler.go @@ -10,9 +10,9 @@ import ( "github.com/NdoleStudio/httpsms/pkg/services" "github.com/NdoleStudio/httpsms/pkg/telemetry" "github.com/NdoleStudio/httpsms/pkg/validators" + "github.com/NdoleStudio/stacktrace" "github.com/davecgh/go-spew/spew" "github.com/gofiber/fiber/v3" - "github.com/palantir/stacktrace" ) // MessageThreadHandler handles message-thead http requests. diff --git a/api/pkg/handlers/message_thread_handler_test.go b/api/pkg/handlers/message_thread_handler_test.go index ab2d75962..60b9d756e 100644 --- a/api/pkg/handlers/message_thread_handler_test.go +++ b/api/pkg/handlers/message_thread_handler_test.go @@ -15,9 +15,9 @@ import ( "github.com/NdoleStudio/httpsms/pkg/services" "github.com/NdoleStudio/httpsms/pkg/telemetry" "github.com/NdoleStudio/httpsms/pkg/validators" + "github.com/NdoleStudio/stacktrace" "github.com/gofiber/fiber/v3" "github.com/google/uuid" - "github.com/palantir/stacktrace" "github.com/stretchr/testify/require" "go.opentelemetry.io/otel/trace" "gorm.io/gorm" diff --git a/api/pkg/handlers/phone_api_key_handler.go b/api/pkg/handlers/phone_api_key_handler.go index 835ad893a..55b981cf4 100644 --- a/api/pkg/handlers/phone_api_key_handler.go +++ b/api/pkg/handlers/phone_api_key_handler.go @@ -9,10 +9,10 @@ import ( "github.com/NdoleStudio/httpsms/pkg/services" "github.com/NdoleStudio/httpsms/pkg/telemetry" "github.com/NdoleStudio/httpsms/pkg/validators" + "github.com/NdoleStudio/stacktrace" "github.com/davecgh/go-spew/spew" "github.com/gofiber/fiber/v3" "github.com/google/uuid" - "github.com/palantir/stacktrace" ) // PhoneAPIKeyHandler handles phone API key http requests diff --git a/api/pkg/handlers/phone_handler.go b/api/pkg/handlers/phone_handler.go index 291c071ad..678b89062 100644 --- a/api/pkg/handlers/phone_handler.go +++ b/api/pkg/handlers/phone_handler.go @@ -10,8 +10,8 @@ import ( "github.com/NdoleStudio/httpsms/pkg/services" "github.com/NdoleStudio/httpsms/pkg/telemetry" + "github.com/NdoleStudio/stacktrace" "github.com/gofiber/fiber/v3" - "github.com/palantir/stacktrace" ) // PhoneHandler handles phone http requests. diff --git a/api/pkg/handlers/user_handler.go b/api/pkg/handlers/user_handler.go index b7066aa3f..8f73ba00c 100644 --- a/api/pkg/handlers/user_handler.go +++ b/api/pkg/handlers/user_handler.go @@ -10,8 +10,8 @@ import ( "github.com/NdoleStudio/httpsms/pkg/services" "github.com/NdoleStudio/httpsms/pkg/telemetry" + "github.com/NdoleStudio/stacktrace" "github.com/gofiber/fiber/v3" - "github.com/palantir/stacktrace" ) // UserHandler handles user http requests. diff --git a/api/pkg/handlers/webhook_handler.go b/api/pkg/handlers/webhook_handler.go index 656d59ea0..6eb389e3c 100644 --- a/api/pkg/handlers/webhook_handler.go +++ b/api/pkg/handlers/webhook_handler.go @@ -11,9 +11,9 @@ import ( "github.com/NdoleStudio/httpsms/pkg/services" "github.com/NdoleStudio/httpsms/pkg/telemetry" "github.com/NdoleStudio/httpsms/pkg/validators" + "github.com/NdoleStudio/stacktrace" "github.com/davecgh/go-spew/spew" "github.com/gofiber/fiber/v3" - "github.com/palantir/stacktrace" ) // WebhookHandler handles webhook requests diff --git a/api/pkg/listeners/billing_listener.go b/api/pkg/listeners/billing_listener.go index 59f166f10..2645e27c3 100644 --- a/api/pkg/listeners/billing_listener.go +++ b/api/pkg/listeners/billing_listener.go @@ -9,8 +9,8 @@ import ( "github.com/NdoleStudio/httpsms/pkg/events" "github.com/NdoleStudio/httpsms/pkg/services" "github.com/NdoleStudio/httpsms/pkg/telemetry" + "github.com/NdoleStudio/stacktrace" cloudevents "github.com/cloudevents/sdk-go/v2" - "github.com/palantir/stacktrace" ) // BillingListener handles cloud events which affect billing diff --git a/api/pkg/listeners/discord_listener.go b/api/pkg/listeners/discord_listener.go index 531aa2080..aebed46b7 100644 --- a/api/pkg/listeners/discord_listener.go +++ b/api/pkg/listeners/discord_listener.go @@ -7,8 +7,8 @@ import ( "github.com/NdoleStudio/httpsms/pkg/events" "github.com/NdoleStudio/httpsms/pkg/services" "github.com/NdoleStudio/httpsms/pkg/telemetry" + "github.com/NdoleStudio/stacktrace" cloudevents "github.com/cloudevents/sdk-go/v2" - "github.com/palantir/stacktrace" ) // DiscordListener sends messages to discord diff --git a/api/pkg/listeners/email_notification_listener.go b/api/pkg/listeners/email_notification_listener.go index 1f7662855..fc6065398 100644 --- a/api/pkg/listeners/email_notification_listener.go +++ b/api/pkg/listeners/email_notification_listener.go @@ -7,8 +7,8 @@ import ( "github.com/NdoleStudio/httpsms/pkg/events" "github.com/NdoleStudio/httpsms/pkg/services" "github.com/NdoleStudio/httpsms/pkg/telemetry" + "github.com/NdoleStudio/stacktrace" cloudevents "github.com/cloudevents/sdk-go/v2" - "github.com/palantir/stacktrace" ) // EmailNotificationListener listens for events about failed and expired messages diff --git a/api/pkg/listeners/heartbeat_listener.go b/api/pkg/listeners/heartbeat_listener.go index 00767a955..eef4933f5 100644 --- a/api/pkg/listeners/heartbeat_listener.go +++ b/api/pkg/listeners/heartbeat_listener.go @@ -4,9 +4,9 @@ import ( "context" "fmt" + "github.com/NdoleStudio/stacktrace" cloudevents "github.com/cloudevents/sdk-go/v2" "github.com/davecgh/go-spew/spew" - "github.com/palantir/stacktrace" "github.com/NdoleStudio/httpsms/pkg/events" "github.com/NdoleStudio/httpsms/pkg/services" diff --git a/api/pkg/listeners/integration_3cx_listener.go b/api/pkg/listeners/integration_3cx_listener.go index 2aafebf9f..529916f86 100644 --- a/api/pkg/listeners/integration_3cx_listener.go +++ b/api/pkg/listeners/integration_3cx_listener.go @@ -7,8 +7,8 @@ import ( "github.com/NdoleStudio/httpsms/pkg/events" "github.com/NdoleStudio/httpsms/pkg/services" "github.com/NdoleStudio/httpsms/pkg/telemetry" + "github.com/NdoleStudio/stacktrace" cloudevents "github.com/cloudevents/sdk-go/v2" - "github.com/palantir/stacktrace" ) // Integration3CXListener sends 3CX events to users diff --git a/api/pkg/listeners/marketing_listener.go b/api/pkg/listeners/marketing_listener.go index c2bc0a20a..4bcf15d11 100644 --- a/api/pkg/listeners/marketing_listener.go +++ b/api/pkg/listeners/marketing_listener.go @@ -7,8 +7,8 @@ import ( "github.com/NdoleStudio/httpsms/pkg/events" "github.com/NdoleStudio/httpsms/pkg/services" "github.com/NdoleStudio/httpsms/pkg/telemetry" + "github.com/NdoleStudio/stacktrace" cloudevents "github.com/cloudevents/sdk-go/v2" - "github.com/palantir/stacktrace" ) // MarketingListener handled marketing events diff --git a/api/pkg/listeners/message_listener.go b/api/pkg/listeners/message_listener.go index b6193626c..49fd18a3d 100644 --- a/api/pkg/listeners/message_listener.go +++ b/api/pkg/listeners/message_listener.go @@ -8,8 +8,8 @@ import ( "github.com/NdoleStudio/httpsms/pkg/events" "github.com/NdoleStudio/httpsms/pkg/services" "github.com/NdoleStudio/httpsms/pkg/telemetry" + "github.com/NdoleStudio/stacktrace" cloudevents "github.com/cloudevents/sdk-go/v2" - "github.com/palantir/stacktrace" ) // MessageListener handles cloud events which need to update entities.Message diff --git a/api/pkg/listeners/message_send_schedule_listener.go b/api/pkg/listeners/message_send_schedule_listener.go index 0fb1f00c0..59f93b847 100644 --- a/api/pkg/listeners/message_send_schedule_listener.go +++ b/api/pkg/listeners/message_send_schedule_listener.go @@ -7,8 +7,8 @@ import ( "github.com/NdoleStudio/httpsms/pkg/events" "github.com/NdoleStudio/httpsms/pkg/services" "github.com/NdoleStudio/httpsms/pkg/telemetry" + "github.com/NdoleStudio/stacktrace" cloudevents "github.com/cloudevents/sdk-go/v2" - "github.com/palantir/stacktrace" ) // MessageSendScheduleListener handles cloud events related to message send schedules. diff --git a/api/pkg/listeners/message_thread_listener.go b/api/pkg/listeners/message_thread_listener.go index 47621296e..ad22e810f 100644 --- a/api/pkg/listeners/message_thread_listener.go +++ b/api/pkg/listeners/message_thread_listener.go @@ -9,8 +9,8 @@ import ( "github.com/NdoleStudio/httpsms/pkg/events" "github.com/NdoleStudio/httpsms/pkg/services" "github.com/NdoleStudio/httpsms/pkg/telemetry" + "github.com/NdoleStudio/stacktrace" cloudevents "github.com/cloudevents/sdk-go/v2" - "github.com/palantir/stacktrace" ) // MessageThreadListener handles cloud events which need to update entities.MessageThread diff --git a/api/pkg/listeners/phone_api_key_listener.go b/api/pkg/listeners/phone_api_key_listener.go index 04fc0ce2e..45333ac56 100644 --- a/api/pkg/listeners/phone_api_key_listener.go +++ b/api/pkg/listeners/phone_api_key_listener.go @@ -6,9 +6,9 @@ import ( "github.com/NdoleStudio/httpsms/pkg/entities" + "github.com/NdoleStudio/stacktrace" cloudevents "github.com/cloudevents/sdk-go/v2" "github.com/davecgh/go-spew/spew" - "github.com/palantir/stacktrace" "github.com/NdoleStudio/httpsms/pkg/events" "github.com/NdoleStudio/httpsms/pkg/services" diff --git a/api/pkg/listeners/phone_listener.go b/api/pkg/listeners/phone_listener.go index 08d1990d8..f39448483 100644 --- a/api/pkg/listeners/phone_listener.go +++ b/api/pkg/listeners/phone_listener.go @@ -4,8 +4,8 @@ import ( "context" "fmt" + "github.com/NdoleStudio/stacktrace" cloudevents "github.com/cloudevents/sdk-go/v2" - "github.com/palantir/stacktrace" "github.com/NdoleStudio/httpsms/pkg/events" "github.com/NdoleStudio/httpsms/pkg/services" diff --git a/api/pkg/listeners/phone_notification_listener.go b/api/pkg/listeners/phone_notification_listener.go index d397cdda7..40452a946 100644 --- a/api/pkg/listeners/phone_notification_listener.go +++ b/api/pkg/listeners/phone_notification_listener.go @@ -9,8 +9,8 @@ import ( "github.com/NdoleStudio/httpsms/pkg/events" "github.com/NdoleStudio/httpsms/pkg/services" "github.com/NdoleStudio/httpsms/pkg/telemetry" + "github.com/NdoleStudio/stacktrace" cloudevents "github.com/cloudevents/sdk-go/v2" - "github.com/palantir/stacktrace" ) // PhoneNotificationListener handles cloud events which sends notifications diff --git a/api/pkg/listeners/user_listener.go b/api/pkg/listeners/user_listener.go index 960ce6c90..e4a15d32f 100644 --- a/api/pkg/listeners/user_listener.go +++ b/api/pkg/listeners/user_listener.go @@ -9,8 +9,8 @@ import ( "github.com/NdoleStudio/httpsms/pkg/events" "github.com/NdoleStudio/httpsms/pkg/services" "github.com/NdoleStudio/httpsms/pkg/telemetry" + "github.com/NdoleStudio/stacktrace" cloudevents "github.com/cloudevents/sdk-go/v2" - "github.com/palantir/stacktrace" ) // UserListener handles cloud events which sends notifications diff --git a/api/pkg/listeners/webhook_listener.go b/api/pkg/listeners/webhook_listener.go index 5ff410467..ff9ab5700 100644 --- a/api/pkg/listeners/webhook_listener.go +++ b/api/pkg/listeners/webhook_listener.go @@ -7,8 +7,8 @@ import ( "github.com/NdoleStudio/httpsms/pkg/events" "github.com/NdoleStudio/httpsms/pkg/services" "github.com/NdoleStudio/httpsms/pkg/telemetry" + "github.com/NdoleStudio/stacktrace" cloudevents "github.com/cloudevents/sdk-go/v2" - "github.com/palantir/stacktrace" ) // WebhookListener sends webhook events to users diff --git a/api/pkg/listeners/websocket_listener.go b/api/pkg/listeners/websocket_listener.go index 8aa9054f0..b06381d78 100644 --- a/api/pkg/listeners/websocket_listener.go +++ b/api/pkg/listeners/websocket_listener.go @@ -4,8 +4,8 @@ import ( "context" "fmt" + "github.com/NdoleStudio/stacktrace" cloudevents "github.com/cloudevents/sdk-go/v2" - "github.com/palantir/stacktrace" "github.com/NdoleStudio/httpsms/pkg/events" "github.com/NdoleStudio/httpsms/pkg/telemetry" diff --git a/api/pkg/middlewares/api_key_auth_middleware.go b/api/pkg/middlewares/api_key_auth_middleware.go index 0d65be307..f2b184194 100644 --- a/api/pkg/middlewares/api_key_auth_middleware.go +++ b/api/pkg/middlewares/api_key_auth_middleware.go @@ -6,8 +6,8 @@ import ( "github.com/NdoleStudio/httpsms/pkg/repositories" "github.com/NdoleStudio/httpsms/pkg/telemetry" + "github.com/NdoleStudio/stacktrace" "github.com/gofiber/fiber/v3" - "github.com/palantir/stacktrace" ) // APIKeyAuth authenticates a user from the X-API-Key header diff --git a/api/pkg/middlewares/bearer_api_key_auth_middleware.go b/api/pkg/middlewares/bearer_api_key_auth_middleware.go index 7806aa7d0..f52d0ba09 100644 --- a/api/pkg/middlewares/bearer_api_key_auth_middleware.go +++ b/api/pkg/middlewares/bearer_api_key_auth_middleware.go @@ -6,8 +6,8 @@ import ( "github.com/NdoleStudio/httpsms/pkg/repositories" "github.com/NdoleStudio/httpsms/pkg/telemetry" + "github.com/NdoleStudio/stacktrace" "github.com/gofiber/fiber/v3" - "github.com/palantir/stacktrace" ) // BearerAPIKeyAuth authenticates an API key using the Bearer header diff --git a/api/pkg/middlewares/bearer_auth_middleware.go b/api/pkg/middlewares/bearer_auth_middleware.go index 1573413c3..6fb57a648 100644 --- a/api/pkg/middlewares/bearer_auth_middleware.go +++ b/api/pkg/middlewares/bearer_auth_middleware.go @@ -8,8 +8,8 @@ import ( "firebase.google.com/go/auth" "github.com/NdoleStudio/httpsms/pkg/entities" "github.com/NdoleStudio/httpsms/pkg/telemetry" + "github.com/NdoleStudio/stacktrace" "github.com/gofiber/fiber/v3" - "github.com/palantir/stacktrace" ) // BearerAuth authenticates a user based on the bearer token diff --git a/api/pkg/middlewares/http_request_logger_middleware.go b/api/pkg/middlewares/http_request_logger_middleware.go index 388c02917..06f5e06da 100644 --- a/api/pkg/middlewares/http_request_logger_middleware.go +++ b/api/pkg/middlewares/http_request_logger_middleware.go @@ -5,8 +5,8 @@ import ( "slices" "github.com/NdoleStudio/httpsms/pkg/telemetry" + "github.com/NdoleStudio/stacktrace" "github.com/gofiber/fiber/v3" - "github.com/palantir/stacktrace" ) const ( diff --git a/api/pkg/middlewares/phone_api_key_auth_middleware.go b/api/pkg/middlewares/phone_api_key_auth_middleware.go index 5fb01c5ba..fb054aa4a 100644 --- a/api/pkg/middlewares/phone_api_key_auth_middleware.go +++ b/api/pkg/middlewares/phone_api_key_auth_middleware.go @@ -6,8 +6,8 @@ import ( "github.com/NdoleStudio/httpsms/pkg/repositories" "github.com/NdoleStudio/httpsms/pkg/telemetry" + "github.com/NdoleStudio/stacktrace" "github.com/gofiber/fiber/v3" - "github.com/palantir/stacktrace" ) // PhoneAPIKeyAuth authenticates a user from the X-API-Key header diff --git a/api/pkg/repositories/google_cloud_storage_attachment_repository.go b/api/pkg/repositories/google_cloud_storage_attachment_repository.go index 34d389eb7..de0f83007 100644 --- a/api/pkg/repositories/google_cloud_storage_attachment_repository.go +++ b/api/pkg/repositories/google_cloud_storage_attachment_repository.go @@ -8,7 +8,7 @@ import ( "cloud.google.com/go/storage" "github.com/NdoleStudio/httpsms/pkg/telemetry" - "github.com/palantir/stacktrace" + "github.com/NdoleStudio/stacktrace" ) // GoogleCloudStorageAttachmentRepository stores attachments in Google Cloud Storage diff --git a/api/pkg/repositories/gorm_billing_usage_repository.go b/api/pkg/repositories/gorm_billing_usage_repository.go index 47c7fadb6..02509e27d 100644 --- a/api/pkg/repositories/gorm_billing_usage_repository.go +++ b/api/pkg/repositories/gorm_billing_usage_repository.go @@ -8,9 +8,9 @@ import ( "github.com/NdoleStudio/httpsms/pkg/entities" "github.com/NdoleStudio/httpsms/pkg/telemetry" + "github.com/NdoleStudio/stacktrace" "github.com/cockroachdb/cockroach-go/v2/crdb/crdbgorm" "github.com/google/uuid" - "github.com/palantir/stacktrace" "gorm.io/gorm" ) diff --git a/api/pkg/repositories/gorm_discord_repository.go b/api/pkg/repositories/gorm_discord_repository.go index a1d70faa4..462ee37b1 100644 --- a/api/pkg/repositories/gorm_discord_repository.go +++ b/api/pkg/repositories/gorm_discord_repository.go @@ -7,8 +7,8 @@ import ( "github.com/NdoleStudio/httpsms/pkg/entities" "github.com/NdoleStudio/httpsms/pkg/telemetry" + "github.com/NdoleStudio/stacktrace" "github.com/google/uuid" - "github.com/palantir/stacktrace" "gorm.io/gorm" ) diff --git a/api/pkg/repositories/gorm_heartbeat_monitor_repository.go b/api/pkg/repositories/gorm_heartbeat_monitor_repository.go index c53e2368b..364e64351 100644 --- a/api/pkg/repositories/gorm_heartbeat_monitor_repository.go +++ b/api/pkg/repositories/gorm_heartbeat_monitor_repository.go @@ -11,7 +11,7 @@ import ( "github.com/NdoleStudio/httpsms/pkg/entities" "github.com/NdoleStudio/httpsms/pkg/telemetry" - "github.com/palantir/stacktrace" + "github.com/NdoleStudio/stacktrace" "gorm.io/gorm" ) diff --git a/api/pkg/repositories/gorm_heartbeat_repository.go b/api/pkg/repositories/gorm_heartbeat_repository.go index 8f77ef21e..544d7eed7 100644 --- a/api/pkg/repositories/gorm_heartbeat_repository.go +++ b/api/pkg/repositories/gorm_heartbeat_repository.go @@ -8,7 +8,7 @@ import ( "github.com/NdoleStudio/httpsms/pkg/entities" "github.com/NdoleStudio/httpsms/pkg/telemetry" - "github.com/palantir/stacktrace" + "github.com/NdoleStudio/stacktrace" "gorm.io/gorm" ) diff --git a/api/pkg/repositories/gorm_integration_3cx_repository.go b/api/pkg/repositories/gorm_integration_3cx_repository.go index f6bbf0f7c..c63e5370b 100644 --- a/api/pkg/repositories/gorm_integration_3cx_repository.go +++ b/api/pkg/repositories/gorm_integration_3cx_repository.go @@ -7,7 +7,7 @@ import ( "github.com/NdoleStudio/httpsms/pkg/entities" "github.com/NdoleStudio/httpsms/pkg/telemetry" - "github.com/palantir/stacktrace" + "github.com/NdoleStudio/stacktrace" "gorm.io/gorm" ) diff --git a/api/pkg/repositories/gorm_message_repository.go b/api/pkg/repositories/gorm_message_repository.go index 0eea42d1d..bb9c55be7 100644 --- a/api/pkg/repositories/gorm_message_repository.go +++ b/api/pkg/repositories/gorm_message_repository.go @@ -9,9 +9,9 @@ import ( "github.com/NdoleStudio/httpsms/pkg/entities" "github.com/NdoleStudio/httpsms/pkg/telemetry" + "github.com/NdoleStudio/stacktrace" "github.com/cockroachdb/cockroach-go/v2/crdb/crdbgorm" "github.com/google/uuid" - "github.com/palantir/stacktrace" "gorm.io/gorm" ) diff --git a/api/pkg/repositories/gorm_message_send_schedule_repository.go b/api/pkg/repositories/gorm_message_send_schedule_repository.go index 54e588431..49538ce05 100644 --- a/api/pkg/repositories/gorm_message_send_schedule_repository.go +++ b/api/pkg/repositories/gorm_message_send_schedule_repository.go @@ -7,8 +7,8 @@ import ( "github.com/NdoleStudio/httpsms/pkg/entities" "github.com/NdoleStudio/httpsms/pkg/telemetry" + "github.com/NdoleStudio/stacktrace" "github.com/google/uuid" - "github.com/palantir/stacktrace" "gorm.io/gorm" ) diff --git a/api/pkg/repositories/gorm_message_thread_repository.go b/api/pkg/repositories/gorm_message_thread_repository.go index 071f5cc79..88506f485 100644 --- a/api/pkg/repositories/gorm_message_thread_repository.go +++ b/api/pkg/repositories/gorm_message_thread_repository.go @@ -11,7 +11,7 @@ import ( "github.com/NdoleStudio/httpsms/pkg/entities" "github.com/NdoleStudio/httpsms/pkg/telemetry" - "github.com/palantir/stacktrace" + "github.com/NdoleStudio/stacktrace" "gorm.io/gorm" ) diff --git a/api/pkg/repositories/gorm_phone_api_key_repository.go b/api/pkg/repositories/gorm_phone_api_key_repository.go index 7f912b28b..51502d1a7 100644 --- a/api/pkg/repositories/gorm_phone_api_key_repository.go +++ b/api/pkg/repositories/gorm_phone_api_key_repository.go @@ -10,9 +10,9 @@ import ( "github.com/NdoleStudio/httpsms/pkg/entities" "github.com/NdoleStudio/httpsms/pkg/telemetry" + "github.com/NdoleStudio/stacktrace" "github.com/dgraph-io/ristretto/v2" "github.com/google/uuid" - "github.com/palantir/stacktrace" "gorm.io/gorm" ) diff --git a/api/pkg/repositories/gorm_phone_notification_repository.go b/api/pkg/repositories/gorm_phone_notification_repository.go index ffe9352e9..1301ecc02 100644 --- a/api/pkg/repositories/gorm_phone_notification_repository.go +++ b/api/pkg/repositories/gorm_phone_notification_repository.go @@ -11,7 +11,7 @@ import ( "github.com/NdoleStudio/httpsms/pkg/entities" "github.com/NdoleStudio/httpsms/pkg/telemetry" - "github.com/palantir/stacktrace" + "github.com/NdoleStudio/stacktrace" "gorm.io/gorm" ) diff --git a/api/pkg/repositories/gorm_phone_repository.go b/api/pkg/repositories/gorm_phone_repository.go index f57cdbe10..98c2f3fb0 100644 --- a/api/pkg/repositories/gorm_phone_repository.go +++ b/api/pkg/repositories/gorm_phone_repository.go @@ -8,9 +8,9 @@ import ( "github.com/NdoleStudio/httpsms/pkg/entities" "github.com/NdoleStudio/httpsms/pkg/telemetry" + "github.com/NdoleStudio/stacktrace" "github.com/dgraph-io/ristretto/v2" "github.com/google/uuid" - "github.com/palantir/stacktrace" "gorm.io/gorm" ) diff --git a/api/pkg/repositories/gorm_user_repository.go b/api/pkg/repositories/gorm_user_repository.go index a2c95e5ef..da46e5e73 100644 --- a/api/pkg/repositories/gorm_user_repository.go +++ b/api/pkg/repositories/gorm_user_repository.go @@ -15,7 +15,7 @@ import ( "github.com/NdoleStudio/httpsms/pkg/entities" "github.com/NdoleStudio/httpsms/pkg/telemetry" - "github.com/palantir/stacktrace" + "github.com/NdoleStudio/stacktrace" "gorm.io/gorm" ) diff --git a/api/pkg/repositories/gorm_webhook_repository.go b/api/pkg/repositories/gorm_webhook_repository.go index 86b2dd1d9..c56bd3a73 100644 --- a/api/pkg/repositories/gorm_webhook_repository.go +++ b/api/pkg/repositories/gorm_webhook_repository.go @@ -7,8 +7,8 @@ import ( "github.com/NdoleStudio/httpsms/pkg/entities" "github.com/NdoleStudio/httpsms/pkg/telemetry" + "github.com/NdoleStudio/stacktrace" "github.com/google/uuid" - "github.com/palantir/stacktrace" "gorm.io/gorm" ) diff --git a/api/pkg/repositories/memory_attachment_repository.go b/api/pkg/repositories/memory_attachment_repository.go index c640d5f8d..eab19336a 100644 --- a/api/pkg/repositories/memory_attachment_repository.go +++ b/api/pkg/repositories/memory_attachment_repository.go @@ -6,7 +6,7 @@ import ( "sync" "github.com/NdoleStudio/httpsms/pkg/telemetry" - "github.com/palantir/stacktrace" + "github.com/NdoleStudio/stacktrace" ) // MemoryAttachmentRepository stores attachments in memory diff --git a/api/pkg/repositories/mongo_heartbeat_monitor_repository.go b/api/pkg/repositories/mongo_heartbeat_monitor_repository.go index c6f44dc89..2f6f71cdb 100644 --- a/api/pkg/repositories/mongo_heartbeat_monitor_repository.go +++ b/api/pkg/repositories/mongo_heartbeat_monitor_repository.go @@ -11,7 +11,7 @@ import ( "github.com/NdoleStudio/httpsms/pkg/entities" "github.com/NdoleStudio/httpsms/pkg/telemetry" - "github.com/palantir/stacktrace" + "github.com/NdoleStudio/stacktrace" ) // mongoHeartbeatMonitorRepository is responsible for persisting entities.HeartbeatMonitor in MongoDB diff --git a/api/pkg/repositories/mongo_heartbeat_repository.go b/api/pkg/repositories/mongo_heartbeat_repository.go index 72bef3b67..e97e696a2 100644 --- a/api/pkg/repositories/mongo_heartbeat_repository.go +++ b/api/pkg/repositories/mongo_heartbeat_repository.go @@ -10,7 +10,7 @@ import ( "github.com/NdoleStudio/httpsms/pkg/entities" "github.com/NdoleStudio/httpsms/pkg/telemetry" - "github.com/palantir/stacktrace" + "github.com/NdoleStudio/stacktrace" ) // mongoHeartbeatRepository is responsible for persisting entities.Heartbeat in MongoDB diff --git a/api/pkg/repositories/mongodb.go b/api/pkg/repositories/mongodb.go index 5f68f9c9b..74ea7446f 100644 --- a/api/pkg/repositories/mongodb.go +++ b/api/pkg/repositories/mongodb.go @@ -6,8 +6,8 @@ import ( "reflect" "time" + "github.com/NdoleStudio/stacktrace" "github.com/google/uuid" - "github.com/palantir/stacktrace" "go.mongodb.org/mongo-driver/v2/bson" "go.mongodb.org/mongo-driver/v2/mongo" "go.mongodb.org/mongo-driver/v2/mongo/options" diff --git a/api/pkg/repositories/repository.go b/api/pkg/repositories/repository.go index 32ba4337d..5d2541fd4 100644 --- a/api/pkg/repositories/repository.go +++ b/api/pkg/repositories/repository.go @@ -3,7 +3,7 @@ package repositories import ( "time" - "github.com/palantir/stacktrace" + "github.com/NdoleStudio/stacktrace" ) // IndexParams parameters for indexing a database table diff --git a/api/pkg/services/billing_service.go b/api/pkg/services/billing_service.go index 262067130..6b5749933 100644 --- a/api/pkg/services/billing_service.go +++ b/api/pkg/services/billing_service.go @@ -11,8 +11,8 @@ import ( "github.com/NdoleStudio/httpsms/pkg/entities" "github.com/NdoleStudio/httpsms/pkg/repositories" "github.com/NdoleStudio/httpsms/pkg/telemetry" + "github.com/NdoleStudio/stacktrace" "github.com/google/uuid" - "github.com/palantir/stacktrace" ) // BillingService is responsible for tracking usages and billing users diff --git a/api/pkg/services/discord_service.go b/api/pkg/services/discord_service.go index 9d5f2680f..e919109b3 100644 --- a/api/pkg/services/discord_service.go +++ b/api/pkg/services/discord_service.go @@ -15,8 +15,8 @@ import ( "github.com/NdoleStudio/httpsms/pkg/entities" "github.com/NdoleStudio/httpsms/pkg/repositories" "github.com/NdoleStudio/httpsms/pkg/telemetry" + "github.com/NdoleStudio/stacktrace" "github.com/google/uuid" - "github.com/palantir/stacktrace" ) // DiscordService is responsible for handling discordIntegrations diff --git a/api/pkg/services/email_notification_service.go b/api/pkg/services/email_notification_service.go index ef6d7d853..a87fcf227 100644 --- a/api/pkg/services/email_notification_service.go +++ b/api/pkg/services/email_notification_service.go @@ -5,7 +5,7 @@ import ( "fmt" "time" - "github.com/palantir/stacktrace" + "github.com/NdoleStudio/stacktrace" "github.com/NdoleStudio/httpsms/pkg/cache" "github.com/NdoleStudio/httpsms/pkg/emails" diff --git a/api/pkg/services/emulator_fcm_client.go b/api/pkg/services/emulator_fcm_client.go index a83767753..adf3ed7c5 100644 --- a/api/pkg/services/emulator_fcm_client.go +++ b/api/pkg/services/emulator_fcm_client.go @@ -10,7 +10,7 @@ import ( "firebase.google.com/go/messaging" "github.com/NdoleStudio/httpsms/pkg/telemetry" - "github.com/palantir/stacktrace" + "github.com/NdoleStudio/stacktrace" ) // EmulatorFCMClient sends FCM messages to the phone emulator via HTTP. diff --git a/api/pkg/services/emulator_push_queue.go b/api/pkg/services/emulator_push_queue.go index f861cebfb..3e99a2369 100644 --- a/api/pkg/services/emulator_push_queue.go +++ b/api/pkg/services/emulator_push_queue.go @@ -6,8 +6,8 @@ import ( "net/http" "time" + "github.com/NdoleStudio/stacktrace" "github.com/carlmjohnson/requests" - "github.com/palantir/stacktrace" "github.com/NdoleStudio/httpsms/pkg/telemetry" "github.com/google/uuid" diff --git a/api/pkg/services/entitlement_service.go b/api/pkg/services/entitlement_service.go index 875b27915..a4a4ab891 100644 --- a/api/pkg/services/entitlement_service.go +++ b/api/pkg/services/entitlement_service.go @@ -9,8 +9,8 @@ import ( "github.com/NdoleStudio/httpsms/pkg/entities" "github.com/NdoleStudio/httpsms/pkg/repositories" "github.com/NdoleStudio/httpsms/pkg/telemetry" + "github.com/NdoleStudio/stacktrace" pluralize "github.com/gertd/go-pluralize" - "github.com/palantir/stacktrace" ) // entityLimits maps entity name → subscription plan → max count. diff --git a/api/pkg/services/event_dispatcher_service.go b/api/pkg/services/event_dispatcher_service.go index 9b2710b59..a090c943a 100644 --- a/api/pkg/services/event_dispatcher_service.go +++ b/api/pkg/services/event_dispatcher_service.go @@ -15,8 +15,8 @@ import ( "github.com/NdoleStudio/httpsms/pkg/events" "github.com/NdoleStudio/httpsms/pkg/telemetry" + "github.com/NdoleStudio/stacktrace" cloudevents "github.com/cloudevents/sdk-go/v2" - "github.com/palantir/stacktrace" ) // EventDispatcher dispatches a new event diff --git a/api/pkg/services/google_cloud_push_queue_service.go b/api/pkg/services/google_cloud_push_queue_service.go index b8854616e..7f9555207 100644 --- a/api/pkg/services/google_cloud_push_queue_service.go +++ b/api/pkg/services/google_cloud_push_queue_service.go @@ -11,7 +11,7 @@ import ( cloudtasks "cloud.google.com/go/cloudtasks/apiv2" "cloud.google.com/go/cloudtasks/apiv2/cloudtaskspb" "github.com/NdoleStudio/httpsms/pkg/telemetry" - "github.com/palantir/stacktrace" + "github.com/NdoleStudio/stacktrace" "google.golang.org/protobuf/types/known/timestamppb" ) diff --git a/api/pkg/services/heartbeat_service.go b/api/pkg/services/heartbeat_service.go index a67899b44..1f02e9c4b 100644 --- a/api/pkg/services/heartbeat_service.go +++ b/api/pkg/services/heartbeat_service.go @@ -11,7 +11,7 @@ import ( "github.com/google/uuid" "github.com/NdoleStudio/httpsms/pkg/repositories" - "github.com/palantir/stacktrace" + "github.com/NdoleStudio/stacktrace" "github.com/NdoleStudio/httpsms/pkg/entities" "github.com/NdoleStudio/httpsms/pkg/telemetry" diff --git a/api/pkg/services/integration_3cx_service.go b/api/pkg/services/integration_3cx_service.go index ef9417df3..b3d046ef7 100644 --- a/api/pkg/services/integration_3cx_service.go +++ b/api/pkg/services/integration_3cx_service.go @@ -15,8 +15,8 @@ import ( "github.com/NdoleStudio/httpsms/pkg/entities" "github.com/NdoleStudio/httpsms/pkg/repositories" "github.com/NdoleStudio/httpsms/pkg/telemetry" + "github.com/NdoleStudio/stacktrace" cloudevents "github.com/cloudevents/sdk-go/v2" - "github.com/palantir/stacktrace" ) // Integration3CXService is responsible for handling webhooks diff --git a/api/pkg/services/lemonsqueezy_service.go b/api/pkg/services/lemonsqueezy_service.go index 40b5aaec0..48347335c 100644 --- a/api/pkg/services/lemonsqueezy_service.go +++ b/api/pkg/services/lemonsqueezy_service.go @@ -12,7 +12,7 @@ import ( "github.com/NdoleStudio/httpsms/pkg/events" "github.com/NdoleStudio/httpsms/pkg/telemetry" "github.com/NdoleStudio/lemonsqueezy-go" - "github.com/palantir/stacktrace" + "github.com/NdoleStudio/stacktrace" ) // LemonsqueezyService is responsible for managing lemonsqueezy events diff --git a/api/pkg/services/marketting_service.go b/api/pkg/services/marketting_service.go index b2fc42109..aabc2d94a 100644 --- a/api/pkg/services/marketting_service.go +++ b/api/pkg/services/marketting_service.go @@ -11,8 +11,8 @@ import ( "github.com/NdoleStudio/httpsms/pkg/entities" "github.com/NdoleStudio/httpsms/pkg/telemetry" plunk "github.com/NdoleStudio/plunk-go" + "github.com/NdoleStudio/stacktrace" "github.com/gofiber/fiber/v3" - "github.com/palantir/stacktrace" ) // MarketingService is handles marketing requests diff --git a/api/pkg/services/message_send_schedule_service.go b/api/pkg/services/message_send_schedule_service.go index 21cfb1273..85627116e 100644 --- a/api/pkg/services/message_send_schedule_service.go +++ b/api/pkg/services/message_send_schedule_service.go @@ -10,8 +10,8 @@ import ( "github.com/NdoleStudio/httpsms/pkg/events" "github.com/NdoleStudio/httpsms/pkg/repositories" "github.com/NdoleStudio/httpsms/pkg/telemetry" + "github.com/NdoleStudio/stacktrace" "github.com/google/uuid" - "github.com/palantir/stacktrace" ) // MessageSendScheduleService manages message send schedules for a user. diff --git a/api/pkg/services/message_service.go b/api/pkg/services/message_service.go index 315c4d8f3..4e6c91128 100644 --- a/api/pkg/services/message_service.go +++ b/api/pkg/services/message_service.go @@ -13,9 +13,9 @@ import ( "github.com/NdoleStudio/httpsms/pkg/events" "github.com/NdoleStudio/httpsms/pkg/repositories" + "github.com/NdoleStudio/stacktrace" cloudevents "github.com/cloudevents/sdk-go/v2" "github.com/google/uuid" - "github.com/palantir/stacktrace" "github.com/NdoleStudio/httpsms/pkg/entities" "github.com/NdoleStudio/httpsms/pkg/telemetry" diff --git a/api/pkg/services/message_thread_service.go b/api/pkg/services/message_thread_service.go index e3dc882c7..687482ddd 100644 --- a/api/pkg/services/message_thread_service.go +++ b/api/pkg/services/message_thread_service.go @@ -11,8 +11,8 @@ import ( "github.com/NdoleStudio/httpsms/pkg/entities" "github.com/NdoleStudio/httpsms/pkg/repositories" "github.com/NdoleStudio/httpsms/pkg/telemetry" + "github.com/NdoleStudio/stacktrace" "github.com/google/uuid" - "github.com/palantir/stacktrace" ) // MessageThreadService is handles message requests diff --git a/api/pkg/services/message_thread_service_test.go b/api/pkg/services/message_thread_service_test.go index fc1e84313..7825be755 100644 --- a/api/pkg/services/message_thread_service_test.go +++ b/api/pkg/services/message_thread_service_test.go @@ -8,8 +8,8 @@ import ( "github.com/NdoleStudio/httpsms/pkg/entities" "github.com/NdoleStudio/httpsms/pkg/repositories" "github.com/NdoleStudio/httpsms/pkg/telemetry" + "github.com/NdoleStudio/stacktrace" "github.com/google/uuid" - "github.com/palantir/stacktrace" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "gorm.io/gorm" diff --git a/api/pkg/services/phone_api_key_service.go b/api/pkg/services/phone_api_key_service.go index 796e42129..9762f116e 100644 --- a/api/pkg/services/phone_api_key_service.go +++ b/api/pkg/services/phone_api_key_service.go @@ -12,8 +12,8 @@ import ( "github.com/NdoleStudio/httpsms/pkg/entities" "github.com/NdoleStudio/httpsms/pkg/repositories" "github.com/NdoleStudio/httpsms/pkg/telemetry" + "github.com/NdoleStudio/stacktrace" "github.com/google/uuid" - "github.com/palantir/stacktrace" ) // PhoneAPIKeyService is responsible for managing entities.PhoneAPIKey diff --git a/api/pkg/services/phone_notification_service.go b/api/pkg/services/phone_notification_service.go index 6e7935fb0..aed806ef1 100644 --- a/api/pkg/services/phone_notification_service.go +++ b/api/pkg/services/phone_notification_service.go @@ -13,8 +13,8 @@ import ( "github.com/NdoleStudio/httpsms/pkg/entities" "github.com/NdoleStudio/httpsms/pkg/repositories" "github.com/NdoleStudio/httpsms/pkg/telemetry" + "github.com/NdoleStudio/stacktrace" "github.com/google/uuid" - "github.com/palantir/stacktrace" "go.opentelemetry.io/otel/trace" ) diff --git a/api/pkg/services/phone_service.go b/api/pkg/services/phone_service.go index 517925231..c61bd1210 100644 --- a/api/pkg/services/phone_service.go +++ b/api/pkg/services/phone_service.go @@ -12,7 +12,7 @@ import ( "github.com/nyaruka/phonenumbers" "github.com/NdoleStudio/httpsms/pkg/repositories" - "github.com/palantir/stacktrace" + "github.com/NdoleStudio/stacktrace" "github.com/NdoleStudio/httpsms/pkg/entities" "github.com/NdoleStudio/httpsms/pkg/telemetry" diff --git a/api/pkg/services/service.go b/api/pkg/services/service.go index f733d8b38..605921fa6 100644 --- a/api/pkg/services/service.go +++ b/api/pkg/services/service.go @@ -7,9 +7,9 @@ import ( "github.com/NdoleStudio/httpsms/pkg/telemetry" "github.com/nyaruka/phonenumbers" + "github.com/NdoleStudio/stacktrace" cloudevents "github.com/cloudevents/sdk-go/v2" "github.com/google/uuid" - "github.com/palantir/stacktrace" ) type service struct{} diff --git a/api/pkg/services/user_service.go b/api/pkg/services/user_service.go index bbd5ad463..265ad10fa 100644 --- a/api/pkg/services/user_service.go +++ b/api/pkg/services/user_service.go @@ -13,8 +13,8 @@ import ( "github.com/NdoleStudio/lemonsqueezy-go" "github.com/NdoleStudio/httpsms/pkg/repositories" + "github.com/NdoleStudio/stacktrace" "github.com/google/uuid" - "github.com/palantir/stacktrace" "github.com/NdoleStudio/httpsms/pkg/entities" "github.com/NdoleStudio/httpsms/pkg/telemetry" diff --git a/api/pkg/services/webhook_service.go b/api/pkg/services/webhook_service.go index b8ff84900..581fa202d 100644 --- a/api/pkg/services/webhook_service.go +++ b/api/pkg/services/webhook_service.go @@ -21,11 +21,11 @@ import ( "github.com/NdoleStudio/httpsms/pkg/entities" "github.com/NdoleStudio/httpsms/pkg/repositories" "github.com/NdoleStudio/httpsms/pkg/telemetry" + "github.com/NdoleStudio/stacktrace" cloudevents "github.com/cloudevents/sdk-go/v2" "github.com/golang-jwt/jwt/v5" "github.com/google/uuid" "github.com/lib/pq" - "github.com/palantir/stacktrace" ) // WebhookService is responsible for handling webhooks diff --git a/api/pkg/telemetry/gorm_logger.go b/api/pkg/telemetry/gorm_logger.go index 2a6ef3e9e..9107afcce 100644 --- a/api/pkg/telemetry/gorm_logger.go +++ b/api/pkg/telemetry/gorm_logger.go @@ -5,7 +5,7 @@ import ( "fmt" "time" - "github.com/palantir/stacktrace" + "github.com/NdoleStudio/stacktrace" "gorm.io/gorm/logger" ) diff --git a/api/pkg/validators/bulk_message_handler_validator.go b/api/pkg/validators/bulk_message_handler_validator.go index 53a18a4e0..8b9b926d2 100644 --- a/api/pkg/validators/bulk_message_handler_validator.go +++ b/api/pkg/validators/bulk_message_handler_validator.go @@ -18,10 +18,10 @@ import ( "github.com/NdoleStudio/httpsms/pkg/requests" "github.com/NdoleStudio/httpsms/pkg/services" "github.com/NdoleStudio/httpsms/pkg/telemetry" + "github.com/NdoleStudio/stacktrace" "github.com/dustin/go-humanize" "github.com/jszwec/csvutil" "github.com/nyaruka/phonenumbers" - "github.com/palantir/stacktrace" ) // BulkMessageHandlerValidator validates models used in handlers.BillingHandler diff --git a/api/pkg/validators/discord_handler_validator.go b/api/pkg/validators/discord_handler_validator.go index f9e486f45..035777ceb 100644 --- a/api/pkg/validators/discord_handler_validator.go +++ b/api/pkg/validators/discord_handler_validator.go @@ -6,7 +6,7 @@ import ( "net/url" "github.com/NdoleStudio/httpsms/pkg/discord" - "github.com/palantir/stacktrace" + "github.com/NdoleStudio/stacktrace" "github.com/NdoleStudio/httpsms/pkg/requests" diff --git a/api/pkg/validators/message_handler_validator.go b/api/pkg/validators/message_handler_validator.go index f48109c80..998b4d804 100644 --- a/api/pkg/validators/message_handler_validator.go +++ b/api/pkg/validators/message_handler_validator.go @@ -11,7 +11,7 @@ import ( "github.com/NdoleStudio/httpsms/pkg/cache" "github.com/NdoleStudio/httpsms/pkg/repositories" "github.com/NdoleStudio/httpsms/pkg/services" - "github.com/palantir/stacktrace" + "github.com/NdoleStudio/stacktrace" "github.com/NdoleStudio/httpsms/pkg/entities" diff --git a/api/pkg/validators/turnstile_token_validator.go b/api/pkg/validators/turnstile_token_validator.go index 8a4d65c68..5d30b4601 100644 --- a/api/pkg/validators/turnstile_token_validator.go +++ b/api/pkg/validators/turnstile_token_validator.go @@ -10,7 +10,7 @@ import ( "time" "github.com/NdoleStudio/httpsms/pkg/telemetry" - "github.com/palantir/stacktrace" + "github.com/NdoleStudio/stacktrace" ) // TurnstileTokenValidator validates the token used to validate captchas from cloudflare diff --git a/api/pkg/validators/user_handler_validator.go b/api/pkg/validators/user_handler_validator.go index 31ec84023..ee163d821 100644 --- a/api/pkg/validators/user_handler_validator.go +++ b/api/pkg/validators/user_handler_validator.go @@ -9,7 +9,7 @@ import ( "github.com/NdoleStudio/httpsms/pkg/requests" "github.com/NdoleStudio/httpsms/pkg/services" "github.com/NdoleStudio/httpsms/pkg/telemetry" - "github.com/palantir/stacktrace" + "github.com/NdoleStudio/stacktrace" "github.com/thedevsaddam/govalidator" ) diff --git a/api/pkg/validators/webhook_handler_validator.go b/api/pkg/validators/webhook_handler_validator.go index 88c7c5ff5..f7b6c101e 100644 --- a/api/pkg/validators/webhook_handler_validator.go +++ b/api/pkg/validators/webhook_handler_validator.go @@ -8,7 +8,7 @@ import ( "github.com/NdoleStudio/httpsms/pkg/entities" "github.com/NdoleStudio/httpsms/pkg/repositories" "github.com/NdoleStudio/httpsms/pkg/services" - "github.com/palantir/stacktrace" + "github.com/NdoleStudio/stacktrace" "github.com/NdoleStudio/httpsms/pkg/requests" From af57e73b5c7e4ab418d764c627803a8d41257893 Mon Sep 17 00:00:00 2001 From: Acho Arnold Ewin Date: Sun, 19 Jul 2026 15:29:49 +0300 Subject: [PATCH 05/88] fix(web): format billing message counts Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 10c01eca-aeb5-4fc2-a546-02cee4261d2b --- web/app/pages/billing/index.vue | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/web/app/pages/billing/index.vue b/web/app/pages/billing/index.vue index 9235a0a5b..d06736e00 100644 --- a/web/app/pages/billing/index.vue +++ b/web/app/pages/billing/index.vue @@ -310,7 +310,10 @@ onMounted(async () => { }}

- {{ totalMessages }}/{{ plan.messagesPerMonth }} messages + {{ formatDecimal(totalMessages) }}/{{ + formatDecimal(plan.messagesPerMonth) + }} + messages

From 129dcedc9a43a490a564155f3332bca6d341dca7 Mon Sep 17 00:00:00 2001 From: Acho Arnold Ewin Date: Sun, 19 Jul 2026 18:51:44 +0300 Subject: [PATCH 06/88] refactor(api): use stacktrace formatting helpers Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: ebe8f80e-c697-4bb6-b456-e13081b5bd0f --- api/go.mod | 2 +- api/go.sum | 2 + api/pkg/cache/memory_cache.go | 2 +- api/pkg/cache/redis_cache.go | 6 +- api/pkg/di/container.go | 74 +++++----- .../hermes_notification_email_factory.go | 16 +-- api/pkg/emails/hermes_user_email_factory.go | 16 +-- api/pkg/emails/smtp_mailer_service.go | 2 +- api/pkg/handlers/attachment_handler.go | 2 +- api/pkg/handlers/billing_handler.go | 8 +- api/pkg/handlers/bulk_message_handler.go | 10 +- api/pkg/handlers/discord_handler.go | 40 +++--- api/pkg/handlers/events_handler.go | 8 +- api/pkg/handlers/heartbeat_handler.go | 14 +- api/pkg/handlers/integration_3cx_handler.go | 6 +- api/pkg/handlers/lemonsqueezy_handler.go | 14 +- api/pkg/handlers/message_handler.go | 74 +++++----- .../handlers/message_send_schedule_handler.go | 14 +- api/pkg/handlers/message_thread_handler.go | 18 +-- .../handlers/message_thread_handler_test.go | 2 +- api/pkg/handlers/phone_api_key_handler.go | 22 +-- api/pkg/handlers/phone_handler.go | 22 +-- api/pkg/handlers/user_handler.go | 30 ++-- api/pkg/handlers/webhook_handler.go | 26 ++-- api/pkg/listeners/billing_listener.go | 12 +- api/pkg/listeners/discord_listener.go | 8 +- .../listeners/email_notification_listener.go | 16 +-- api/pkg/listeners/heartbeat_listener.go | 20 +-- api/pkg/listeners/integration_3cx_listener.go | 20 +-- api/pkg/listeners/marketing_listener.go | 8 +- api/pkg/listeners/message_listener.go | 52 +++---- .../message_send_schedule_listener.go | 4 +- api/pkg/listeners/message_thread_listener.go | 44 +++--- api/pkg/listeners/phone_api_key_listener.go | 12 +- api/pkg/listeners/phone_listener.go | 8 +- .../listeners/phone_notification_listener.go | 24 ++-- api/pkg/listeners/user_listener.go | 28 ++-- api/pkg/listeners/webhook_listener.go | 36 ++--- api/pkg/listeners/websocket_listener.go | 20 +-- .../middlewares/api_key_auth_middleware.go | 2 +- .../bearer_api_key_auth_middleware.go | 2 +- api/pkg/middlewares/bearer_auth_middleware.go | 2 +- .../http_request_logger_middleware.go | 2 +- .../phone_api_key_auth_middleware.go | 2 +- ...gle_cloud_storage_attachment_repository.go | 12 +- .../gorm_billing_usage_repository.go | 8 +- .../repositories/gorm_discord_repository.go | 18 +-- .../gorm_heartbeat_monitor_repository.go | 18 +-- .../repositories/gorm_heartbeat_repository.go | 10 +- .../gorm_integration_3cx_repository.go | 8 +- .../repositories/gorm_message_repository.go | 30 ++-- .../gorm_message_send_schedule_repository.go | 16 +-- .../gorm_message_thread_repository.go | 26 ++-- .../gorm_phone_api_key_repository.go | 30 ++-- .../gorm_phone_notification_repository.go | 18 +-- api/pkg/repositories/gorm_phone_repository.go | 22 +-- api/pkg/repositories/gorm_user_repository.go | 38 ++--- .../repositories/gorm_webhook_repository.go | 14 +- .../memory_attachment_repository.go | 2 +- .../mongo_heartbeat_monitor_repository.go | 16 +-- .../mongo_heartbeat_repository.go | 12 +- api/pkg/repositories/mongodb.go | 16 +-- api/pkg/services/billing_service.go | 24 ++-- api/pkg/services/discord_service.go | 30 ++-- .../services/email_notification_service.go | 26 ++-- api/pkg/services/emulator_fcm_client.go | 12 +- api/pkg/services/emulator_push_queue.go | 2 +- api/pkg/services/entitlement_service.go | 4 +- api/pkg/services/event_dispatcher_service.go | 14 +- .../google_cloud_push_queue_service.go | 4 +- api/pkg/services/heartbeat_service.go | 48 +++---- api/pkg/services/integration_3cx_service.go | 24 ++-- api/pkg/services/lemonsqueezy_service.go | 28 ++-- api/pkg/services/marketting_service.go | 8 +- .../services/message_send_schedule_service.go | 12 +- api/pkg/services/message_service.go | 130 +++++++++--------- api/pkg/services/message_thread_service.go | 32 ++--- .../services/message_thread_service_test.go | 4 +- api/pkg/services/phone_api_key_service.go | 30 ++-- .../services/phone_notification_service.go | 44 +++--- api/pkg/services/phone_service.go | 28 ++-- api/pkg/services/service.go | 6 +- api/pkg/services/user_service.go | 76 +++++----- api/pkg/services/webhook_service.go | 40 +++--- api/pkg/telemetry/gorm_logger.go | 2 +- .../bulk_message_handler_validator.go | 16 +-- .../validators/discord_handler_validator.go | 8 +- .../validators/message_handler_validator.go | 4 +- .../validators/turnstile_token_validator.go | 10 +- api/pkg/validators/user_handler_validator.go | 2 +- 90 files changed, 868 insertions(+), 866 deletions(-) diff --git a/api/go.mod b/api/go.mod index 649489f65..094bb1ac4 100644 --- a/api/go.mod +++ b/api/go.mod @@ -12,7 +12,7 @@ require ( github.com/NdoleStudio/go-otelroundtripper v0.0.15 github.com/NdoleStudio/lemonsqueezy-go v1.3.1 github.com/NdoleStudio/plunk-go v0.0.2 - github.com/NdoleStudio/stacktrace v1.0.0 + github.com/NdoleStudio/stacktrace v1.1.0 github.com/avast/retry-go/v5 v5.0.0 github.com/axiomhq/axiom-go v0.32.0 github.com/carlmjohnson/requests v0.25.1 diff --git a/api/go.sum b/api/go.sum index 264094f1a..d0272fa1d 100644 --- a/api/go.sum +++ b/api/go.sum @@ -62,6 +62,8 @@ github.com/NdoleStudio/plunk-go v0.0.2 h1:afPW7MHK4Z3rsybpJBnmTmxKCLKF1M7sPI+BNG github.com/NdoleStudio/plunk-go v0.0.2/go.mod h1:pqG3zKhpn/A2bL1K+WsWzvfTpOeSkYgXhNk5H65uEc8= github.com/NdoleStudio/stacktrace v1.0.0 h1:YIDg0twM2BXRKIFjorX7v2dC0l8z4VEITurI+MNqvVQ= github.com/NdoleStudio/stacktrace v1.0.0/go.mod h1:WOBcbXShYu+v8JtjFEM+rRV9GJW50m+dwIvp4sLuVTg= +github.com/NdoleStudio/stacktrace v1.1.0 h1:Ai0j//4tDo/So3nR8bZgS3pQ4NYmViHlarpIReiyb5k= +github.com/NdoleStudio/stacktrace v1.1.0/go.mod h1:WOBcbXShYu+v8JtjFEM+rRV9GJW50m+dwIvp4sLuVTg= github.com/PuerkitoBio/goquery v1.12.0 h1:pAcL4g3WRXekcB9AU/y1mbKez2dbY2AajVhtkO8RIBo= github.com/PuerkitoBio/goquery v1.12.0/go.mod h1:802ej+gV2y7bbIhOIoPY5sT183ZW0YFofScC4q/hIpQ= github.com/andybalholm/brotli v1.2.1 h1:R+f5xP285VArJDRgowrfb9DqL18yVK0gKAW/F+eTWro= diff --git a/api/pkg/cache/memory_cache.go b/api/pkg/cache/memory_cache.go index e749ca3e3..a5b0487d7 100644 --- a/api/pkg/cache/memory_cache.go +++ b/api/pkg/cache/memory_cache.go @@ -30,7 +30,7 @@ func (cache *memoryCache) Get(ctx context.Context, key string) (value string, er response, ok := cache.store.Get(key) if !ok { - return "", stacktrace.NewError("no item found in cache with key [%s]", key) + return "", stacktrace.NewErrorf("no item found in cache with key [%s]", key) } return response.(string), nil diff --git a/api/pkg/cache/redis_cache.go b/api/pkg/cache/redis_cache.go index 990f09bcd..46d9044bd 100644 --- a/api/pkg/cache/redis_cache.go +++ b/api/pkg/cache/redis_cache.go @@ -31,10 +31,10 @@ func (cache *redisCache) Get(ctx context.Context, key string) (value string, err response, err := cache.client.Get(ctx, key).Result() if errors.Is(err, redis.Nil) { - return "", stacktrace.Propagate(err, "no item found in redis with key [%s]", key) + return "", stacktrace.Propagatef(err, "no item found in redis with key [%s]", key) } if err != nil { - return "", stacktrace.Propagate(err, "cannot get item in redis with key [%s]", key) + return "", stacktrace.Propagatef(err, "cannot get item in redis with key [%s]", key) } return response, nil } @@ -46,7 +46,7 @@ func (cache *redisCache) Set(ctx context.Context, key string, value string, ttl err := cache.client.Set(ctx, key, value, ttl).Err() if err != nil { - return cache.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot set item in redis")) + return cache.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot set item in redis")) } return nil } diff --git a/api/pkg/di/container.go b/api/pkg/di/container.go index 6d933ae86..ad9a18858 100644 --- a/api/pkg/di/container.go +++ b/api/pkg/di/container.go @@ -268,7 +268,7 @@ func (container *Container) DedicatedDB() (db *gorm.DB) { } if err = db.Use(tracing.NewPlugin()); err != nil { - container.logger.Fatal(stacktrace.Propagate(err, "cannot use GORM tracing plugin")) + container.logger.Fatal(stacktrace.Propagatef(err, "cannot use GORM tracing plugin")) } container.dedicatedDB = db @@ -279,11 +279,11 @@ func (container *Container) DedicatedDB() (db *gorm.DB) { container.logger.Debug(fmt.Sprintf("Running migrations for dedicated [%T]", db)) if err = db.AutoMigrate(&entities.Heartbeat{}); err != nil { - container.logger.Fatal(stacktrace.Propagate(err, "cannot migrate %T", &entities.Heartbeat{})) + container.logger.Fatal(stacktrace.Propagatef(err, "cannot migrate %T", &entities.Heartbeat{})) } if err = db.AutoMigrate(&entities.HeartbeatMonitor{}); err != nil { - container.logger.Fatal(stacktrace.Propagate(err, "cannot migrate %T", &entities.HeartbeatMonitor{})) + container.logger.Fatal(stacktrace.Propagatef(err, "cannot migrate %T", &entities.HeartbeatMonitor{})) } return container.dedicatedDB @@ -326,7 +326,7 @@ func (container *Container) DBWithoutMigration() (db *gorm.DB) { container.db = db if err = db.Use(tracing.NewPlugin()); err != nil { - container.logger.Fatal(stacktrace.Propagate(err, "cannot use GORM tracing plugin")) + container.logger.Fatal(stacktrace.Propagatef(err, "cannot use GORM tracing plugin")) } return container.db } @@ -351,7 +351,7 @@ func (container *Container) DB() (db *gorm.DB) { container.db = db if err = db.Use(tracing.NewPlugin()); err != nil { - container.logger.Fatal(stacktrace.Propagate(err, "cannot use GORM tracing plugin")) + container.logger.Fatal(stacktrace.Propagatef(err, "cannot use GORM tracing plugin")) } if os.Getenv("DATABASE_MIGRATION_SKIP") != "" { @@ -371,47 +371,47 @@ ALTER TABLE discords ADD CONSTRAINT IF NOT EXISTS uni_discords_server_id CHECK ( } if err = db.AutoMigrate(&entities.Message{}); err != nil { - container.logger.Fatal(stacktrace.Propagate(err, "cannot migrate %T", &entities.Message{})) + container.logger.Fatal(stacktrace.Propagatef(err, "cannot migrate %T", &entities.Message{})) } if err = db.AutoMigrate(&entities.MessageThread{}); err != nil { - container.logger.Fatal(stacktrace.Propagate(err, "cannot migrate %T", &entities.MessageThread{})) + container.logger.Fatal(stacktrace.Propagatef(err, "cannot migrate %T", &entities.MessageThread{})) } if err = db.AutoMigrate(&entities.User{}); err != nil { - container.logger.Fatal(stacktrace.Propagate(err, "cannot migrate %T", &entities.User{})) + container.logger.Fatal(stacktrace.Propagatef(err, "cannot migrate %T", &entities.User{})) } if err = db.AutoMigrate(&entities.MessageSendSchedule{}); err != nil { - container.logger.Fatal(stacktrace.Propagate(err, "cannot migrate %T", &entities.MessageSendSchedule{})) + container.logger.Fatal(stacktrace.Propagatef(err, "cannot migrate %T", &entities.MessageSendSchedule{})) } if err = db.AutoMigrate(&entities.Phone{}); err != nil { - container.logger.Fatal(stacktrace.Propagate(err, "cannot migrate %T", &entities.Phone{})) + container.logger.Fatal(stacktrace.Propagatef(err, "cannot migrate %T", &entities.Phone{})) } if err = db.AutoMigrate(&entities.PhoneNotification{}); err != nil { - container.logger.Fatal(stacktrace.Propagate(err, "cannot migrate %T", &entities.PhoneNotification{})) + container.logger.Fatal(stacktrace.Propagatef(err, "cannot migrate %T", &entities.PhoneNotification{})) } if err = db.AutoMigrate(&entities.BillingUsage{}); err != nil { - container.logger.Fatal(stacktrace.Propagate(err, "cannot migrate %T", &entities.BillingUsage{})) + container.logger.Fatal(stacktrace.Propagatef(err, "cannot migrate %T", &entities.BillingUsage{})) } if err = db.AutoMigrate(&entities.Webhook{}); err != nil { - container.logger.Fatal(stacktrace.Propagate(err, "cannot migrate %T", &entities.Webhook{})) + container.logger.Fatal(stacktrace.Propagatef(err, "cannot migrate %T", &entities.Webhook{})) } if err = db.AutoMigrate(&entities.Discord{}); err != nil { - container.logger.Fatal(stacktrace.Propagate(err, "cannot migrate %T", &entities.Discord{})) + container.logger.Fatal(stacktrace.Propagatef(err, "cannot migrate %T", &entities.Discord{})) } if err = db.AutoMigrate(&entities.Integration3CX{}); err != nil { - container.logger.Fatal(stacktrace.Propagate(err, "cannot migrate %T", &entities.Integration3CX{})) + container.logger.Fatal(stacktrace.Propagatef(err, "cannot migrate %T", &entities.Integration3CX{})) } if err = db.AutoMigrate(&entities.PhoneAPIKey{}); err != nil { - container.logger.Fatal(stacktrace.Propagate(err, "cannot migrate %T", &entities.PhoneAPIKey{})) + container.logger.Fatal(stacktrace.Propagatef(err, "cannot migrate %T", &entities.PhoneAPIKey{})) } return container.db @@ -423,7 +423,7 @@ func (container *Container) FirebaseApp() (app *firebase.App) { app, err := firebase.NewApp(context.Background(), nil, option.WithCredentialsJSON(container.FirebaseCredentials())) if err != nil { - container.logger.Fatal(stacktrace.Propagate(err, "cannot initialize firebase application")) + container.logger.Fatal(stacktrace.Propagatef(err, "cannot initialize firebase application")) } return app } @@ -444,7 +444,7 @@ func (container *Container) Cache() cache.Cache { container.logger.Debug("creating cache.Cache") opt, err := redis.ParseURL(os.Getenv("REDIS_URL")) if err != nil { - container.logger.Fatal(stacktrace.Propagate(err, "cannot parse redis url [%s]", os.Getenv("REDIS_URL"))) + container.logger.Fatal(stacktrace.Propagatef(err, "cannot parse redis url [%s]", os.Getenv("REDIS_URL"))) } if strings.HasPrefix(os.Getenv("REDIS_URL"), "rediss://") { opt.TLSConfig = &tls.Config{ @@ -456,12 +456,12 @@ func (container *Container) Cache() cache.Cache { // Enable tracing instrumentation. if err = redisotel.InstrumentTracing(redisClient); err != nil { - container.logger.Error(stacktrace.Propagate(err, "cannot instrument redis tracing")) + container.logger.Error(stacktrace.Propagatef(err, "cannot instrument redis tracing")) } // Enable metrics instrumentation. if err = redisotel.InstrumentMetrics(redisClient); err != nil { - container.logger.Fatal(stacktrace.Propagate(err, "cannot instrument redis metrics")) + container.logger.Fatal(stacktrace.Propagatef(err, "cannot instrument redis metrics")) } return cache.NewRedisCache(container.Tracer(), redisClient) @@ -472,7 +472,7 @@ func (container *Container) FirebaseAuthClient() (client *auth.Client) { container.logger.Debug(fmt.Sprintf("creating %T", client)) authClient, err := container.FirebaseApp().Auth(context.Background()) if err != nil { - container.logger.Fatal(stacktrace.Propagate(err, "cannot initialize firebase auth client")) + container.logger.Fatal(stacktrace.Propagatef(err, "cannot initialize firebase auth client")) } return authClient } @@ -483,7 +483,7 @@ func (container *Container) CloudTasksClient() (client *cloudtasks.Client) { client, err := cloudtasks.NewClient(context.Background(), option.WithCredentialsJSON(container.FirebaseCredentials())) if err != nil { - container.logger.Fatal(stacktrace.Propagate(err, "cannot initialize cloud tasks client")) + container.logger.Fatal(stacktrace.Propagatef(err, "cannot initialize cloud tasks client")) } return client @@ -551,7 +551,7 @@ func (container *Container) FCMClient() services.FCMClient { container.logger.Debug("creating FirebaseFCMClient") messagingClient, err := container.FirebaseApp().Messaging(context.Background()) if err != nil { - container.logger.Fatal(stacktrace.Propagate(err, "cannot initialize firebase messaging client")) + container.logger.Fatal(stacktrace.Propagatef(err, "cannot initialize firebase messaging client")) } return services.NewFirebaseFCMClient(messagingClient) } @@ -745,7 +745,7 @@ func (container *Container) Float64Histogram(name, unit, description string) ote ) histogram, err := meter.Float64Histogram(name, otelMetric.WithUnit(unit), otelMetric.WithDescription(description)) if err != nil { - container.logger.Fatal(stacktrace.Propagate(err, "cannot create float64 histogram")) + container.logger.Fatal(stacktrace.Propagatef(err, "cannot create float64 histogram")) } return histogram } @@ -1525,7 +1525,7 @@ func (container *Container) RegisterWebsocketListeners() { container.logger.Debug(fmt.Sprintf("registering listeners for %T", listeners.WebsocketListener{})) if os.Getenv("PUSHER_SECRET") == "" { - container.logger.Warn(stacktrace.NewError("skipping websocket listeners because the PUSHER_SECRET env variable is not set")) + container.logger.Warn(stacktrace.NewErrorf("skipping websocket listeners because the PUSHER_SECRET env variable is not set")) return } @@ -1579,7 +1579,7 @@ func (container *Container) AttachmentRepository() repositories.AttachmentReposi container.logger.Debug("creating GoogleCloudStorageAttachmentRepository") client, err := storage.NewClient(context.Background(), option.WithAuthCredentialsJSON(option.ServiceAccount, container.FirebaseCredentials())) if err != nil { - container.logger.Fatal(stacktrace.Propagate(err, "cannot create GCS client")) + container.logger.Fatal(stacktrace.Propagatef(err, "cannot create GCS client")) } container.attachmentRepository = repositories.NewGoogleCloudStorageAttachmentRepository( container.Logger(), @@ -1767,7 +1767,7 @@ func (container *Container) PhoneRistrettoCache() *ristretto.Cache[string, *enti BufferItems: 64, }) if err != nil { - container.logger.Fatal(stacktrace.Propagate(err, "cannot create phone ristretto cache")) + container.logger.Fatal(stacktrace.Propagatef(err, "cannot create phone ristretto cache")) } container.phoneRistrettoCache = ristrettoCache return container.phoneRistrettoCache @@ -1785,7 +1785,7 @@ func (container *Container) UserRistrettoCache() *ristretto.Cache[string, entiti BufferItems: 64, }) if err != nil { - container.logger.Fatal(stacktrace.Propagate(err, "cannot create user ristretto cache")) + container.logger.Fatal(stacktrace.Propagatef(err, "cannot create user ristretto cache")) } container.userRistrettoCache = ristrettoCache return ristrettoCache @@ -1801,7 +1801,7 @@ func (container *Container) initializeGoogleTraceProvider(version string, namesp traceExporter, err := cloudtrace.New(cloudtrace.WithProjectID(os.Getenv("GCP_PROJECT_ID"))) if err != nil { - container.logger.Fatal(stacktrace.Propagate(err, "cannot create cloud trace traceExporter")) + container.logger.Fatal(stacktrace.Propagatef(err, "cannot create cloud trace traceExporter")) } tp := trace.NewTracerProvider( @@ -1813,7 +1813,7 @@ func (container *Container) initializeGoogleTraceProvider(version string, namesp metricExporter, err := mexporter.New(mexporter.WithProjectID(os.Getenv("GCP_PROJECT_ID"))) if err != nil { - container.logger.Fatal(stacktrace.Propagate(err, "cannot create cloud metric traceExporter")) + container.logger.Fatal(stacktrace.Propagatef(err, "cannot create cloud metric traceExporter")) } meterProvider := metric.NewMeterProvider( @@ -1824,10 +1824,10 @@ func (container *Container) initializeGoogleTraceProvider(version string, namesp return func() { if err = metricExporter.Shutdown(context.Background()); err != nil { - container.logger.Error(stacktrace.Propagate(err, "cannot shutdown cloud metric metric exporter")) + container.logger.Error(stacktrace.Propagatef(err, "cannot shutdown cloud metric metric exporter")) } if err = traceExporter.Shutdown(context.Background()); err != nil { - container.logger.Error(stacktrace.Propagate(err, "cannot shutdown cloud trace trace exporter")) + container.logger.Error(stacktrace.Propagatef(err, "cannot shutdown cloud trace trace exporter")) } } } @@ -1846,7 +1846,7 @@ func (container *Container) initializeAxiomTraceProvider(version string, namespa otlptracehttp.WithHeaders(traceHeaders), ) if err != nil { - container.logger.Fatal(stacktrace.Propagate(err, "cannot create axiom OTLP trace exporter")) + container.logger.Fatal(stacktrace.Propagatef(err, "cannot create axiom OTLP trace exporter")) } tp := trace.NewTracerProvider( @@ -1872,7 +1872,7 @@ func (container *Container) initializeAxiomTraceProvider(version string, namespa otlpmetrichttp.WithHeaders(metricHeaders), ) if err != nil { - container.logger.Fatal(stacktrace.Propagate(err, "cannot create axiom OTLP metric exporter")) + container.logger.Fatal(stacktrace.Propagatef(err, "cannot create axiom OTLP metric exporter")) } meterProvider := metric.NewMeterProvider( @@ -1883,10 +1883,10 @@ func (container *Container) initializeAxiomTraceProvider(version string, namespa return func() { if err := tp.Shutdown(context.Background()); err != nil { - container.logger.Error(stacktrace.Propagate(err, "cannot shutdown axiom trace provider")) + container.logger.Error(stacktrace.Propagatef(err, "cannot shutdown axiom trace provider")) } if err := meterProvider.Shutdown(context.Background()); err != nil { - container.logger.Error(stacktrace.Propagate(err, "cannot shutdown axiom meter provider")) + container.logger.Error(stacktrace.Propagatef(err, "cannot shutdown axiom meter provider")) } } } @@ -1937,7 +1937,7 @@ func axiomLogger(skipFrameCount int) *zerodriver.Logger { axiomzerolog.SetDataset(os.Getenv("AXIOM_DATASET_EVENTS")), ) if err != nil { - log.Fatal(stacktrace.Propagate(err, "cannot create axiom zerolog writer")) + log.Fatal(stacktrace.Propagatef(err, "cannot create axiom zerolog writer")) } zl := zerolog.New(axiomWriter).With().Timestamp().CallerWithSkipFrameCount(skipFrameCount).Logger() diff --git a/api/pkg/emails/hermes_notification_email_factory.go b/api/pkg/emails/hermes_notification_email_factory.go index 4c0440646..d80515e72 100644 --- a/api/pkg/emails/hermes_notification_email_factory.go +++ b/api/pkg/emails/hermes_notification_email_factory.go @@ -62,12 +62,12 @@ func (factory *hermesNotificationEmailFactory) DiscordSendFailed(user *entities. html, err := factory.generator.GenerateHTML(email) if err != nil { - return nil, stacktrace.Propagate(err, "cannot generate html email") + return nil, stacktrace.Propagatef(err, "cannot generate html email") } text, err := factory.generator.GeneratePlainText(email) if err != nil { - return nil, stacktrace.Propagate(err, "cannot generate text email") + return nil, stacktrace.Propagatef(err, "cannot generate text email") } return &Email{ @@ -120,7 +120,7 @@ func (factory *hermesNotificationEmailFactory) WebhookSendFailed(user *entities. html, err := factory.generator.GenerateHTML(email) if err != nil { - return nil, stacktrace.Propagate(err, "cannot generate html email") + return nil, stacktrace.Propagatef(err, "cannot generate html email") } eventPayloadEntryIndex := len(email.Body.Dictionary) - 1 @@ -129,7 +129,7 @@ func (factory *hermesNotificationEmailFactory) WebhookSendFailed(user *entities. return factory.generator.GeneratePlainText(email) }, formattedPayload) if err != nil { - return nil, stacktrace.Propagate(err, "cannot generate text email") + return nil, stacktrace.Propagatef(err, "cannot generate text email") } return &Email{ @@ -198,12 +198,12 @@ func (factory *hermesNotificationEmailFactory) MessageExpired(user *entities.Use html, err := factory.generator.GenerateHTML(email) if err != nil { - return nil, stacktrace.Propagate(err, "cannot generate html email") + return nil, stacktrace.Propagatef(err, "cannot generate html email") } text, err := factory.generator.GeneratePlainText(email) if err != nil { - return nil, stacktrace.Propagate(err, "cannot generate text email") + return nil, stacktrace.Propagatef(err, "cannot generate text email") } return &Email{ @@ -249,12 +249,12 @@ func (factory *hermesNotificationEmailFactory) MessageFailed(user *entities.User html, err := factory.generator.GenerateHTML(email) if err != nil { - return nil, stacktrace.Propagate(err, "cannot generate html email") + return nil, stacktrace.Propagatef(err, "cannot generate html email") } text, err := factory.generator.GeneratePlainText(email) if err != nil { - return nil, stacktrace.Propagate(err, "cannot generate text email") + return nil, stacktrace.Propagatef(err, "cannot generate text email") } return &Email{ diff --git a/api/pkg/emails/hermes_user_email_factory.go b/api/pkg/emails/hermes_user_email_factory.go index 8070a78e7..7f2ef25cb 100644 --- a/api/pkg/emails/hermes_user_email_factory.go +++ b/api/pkg/emails/hermes_user_email_factory.go @@ -52,12 +52,12 @@ func (factory *hermesUserEmailFactory) APIKeyRotated(emailAddress string, timest html, err := factory.generator.GenerateHTML(email) if err != nil { - return nil, stacktrace.Propagate(err, "cannot generate html email") + return nil, stacktrace.Propagatef(err, "cannot generate html email") } text, err := factory.generator.GeneratePlainText(email) if err != nil { - return nil, stacktrace.Propagate(err, "cannot generate text email") + return nil, stacktrace.Propagatef(err, "cannot generate text email") } return &Email{ @@ -97,12 +97,12 @@ func (factory *hermesUserEmailFactory) UsageLimitExceeded(user *entities.User, u html, err := factory.generator.GenerateHTML(email) if err != nil { - return nil, stacktrace.Propagate(err, "cannot generate html email") + return nil, stacktrace.Propagatef(err, "cannot generate html email") } text, err := factory.generator.GeneratePlainText(email) if err != nil { - return nil, stacktrace.Propagate(err, "cannot generate text email") + return nil, stacktrace.Propagatef(err, "cannot generate text email") } return &Email{ @@ -143,12 +143,12 @@ func (factory *hermesUserEmailFactory) UsageLimitAlert(user *entities.User, usag html, err := factory.generator.GenerateHTML(email) if err != nil { - return nil, stacktrace.Propagate(err, "cannot generate html email") + return nil, stacktrace.Propagatef(err, "cannot generate html email") } text, err := factory.generator.GeneratePlainText(email) if err != nil { - return nil, stacktrace.Propagate(err, "cannot generate text email") + return nil, stacktrace.Propagatef(err, "cannot generate text email") } return &Email{ @@ -201,12 +201,12 @@ func (factory *hermesUserEmailFactory) PhoneDead(user *entities.User, lastHeartb html, err := factory.generator.GenerateHTML(email) if err != nil { - return nil, stacktrace.Propagate(err, "cannot generate html email") + return nil, stacktrace.Propagatef(err, "cannot generate html email") } text, err := factory.generator.GeneratePlainText(email) if err != nil { - return nil, stacktrace.Propagate(err, "cannot generate text email") + return nil, stacktrace.Propagatef(err, "cannot generate text email") } return &Email{ diff --git a/api/pkg/emails/smtp_mailer_service.go b/api/pkg/emails/smtp_mailer_service.go index 5f57d1daf..ffe18432f 100644 --- a/api/pkg/emails/smtp_mailer_service.go +++ b/api/pkg/emails/smtp_mailer_service.go @@ -51,7 +51,7 @@ func (mailer *smtpMailer) Send(ctx context.Context, email *Email) (err error) { err = e.Send(mailer.address, mailer.auth) if err != nil { - return stacktrace.Propagate(err, "cannot send email") + return stacktrace.Propagatef(err, "cannot send email") } return nil diff --git a/api/pkg/handlers/attachment_handler.go b/api/pkg/handlers/attachment_handler.go index e73f12530..696521b61 100644 --- a/api/pkg/handlers/attachment_handler.go +++ b/api/pkg/handlers/attachment_handler.go @@ -66,7 +66,7 @@ func (h *AttachmentHandler) GetAttachment(c fiber.Ctx) error { data, err := h.storage.Download(ctx, path) if err != nil { - ctxLogger.Warn(stacktrace.Propagate(err, "cannot download attachment from path [%s]", path)) + ctxLogger.Warn(stacktrace.Propagatef(err, "cannot download attachment from path [%s]", path)) if stacktrace.GetCode(err) == repositories.ErrCodeNotFound { return h.responseNotFound(c, "attachment not found") } diff --git a/api/pkg/handlers/billing_handler.go b/api/pkg/handlers/billing_handler.go index a33ad2312..bcbdef4ee 100644 --- a/api/pkg/handlers/billing_handler.go +++ b/api/pkg/handlers/billing_handler.go @@ -65,18 +65,18 @@ func (h *BillingHandler) UsageHistory(c fiber.Ctx) error { var request requests.BillingUsageHistory if err := c.Bind().Query(&request); err != nil { - ctxLogger.Warn(stacktrace.Propagate(err, "cannot marshall params [%s] into %T", c.Body(), request)) + ctxLogger.Warn(stacktrace.Propagatef(err, "cannot marshall params [%s] into %T", c.Body(), request)) return h.responseBadRequest(c, err) } if errors := h.validator.ValidateHistory(ctx, request.Sanitize()); len(errors) != 0 { - ctxLogger.Warn(stacktrace.NewError("validation errors [%s], while fetching heartbeats [%+#v]", spew.Sdump(errors), request)) + ctxLogger.Warn(stacktrace.NewErrorf("validation errors [%s], while fetching heartbeats [%+#v]", spew.Sdump(errors), request)) return h.responseUnprocessableEntity(c, errors, "validation errors while fetching usage history") } heartbeats, err := h.service.GetUsageHistory(ctx, h.userIDFomContext(c), request.ToIndexParams()) if err != nil { - ctxLogger.Error(stacktrace.Propagate(err, "cannot get billing usage history with params [%+#v]", request)) + ctxLogger.Error(stacktrace.Propagatef(err, "cannot get billing usage history with params [%+#v]", request)) return h.responseInternalServerError(c) } @@ -104,7 +104,7 @@ func (h *BillingHandler) Usage(c fiber.Ctx) error { billingUsage, err := h.service.GetCurrentUsage(ctx, h.userIDFomContext(c)) if err != nil { - ctxLogger.Error(stacktrace.Propagate(err, "cannot get current usage record for user [%s]", h.userFromContext(c))) + ctxLogger.Error(stacktrace.Propagatef(err, "cannot get current usage record for user [%s]", h.userFromContext(c))) return h.responseInternalServerError(c) } diff --git a/api/pkg/handlers/bulk_message_handler.go b/api/pkg/handlers/bulk_message_handler.go index 28e0d0ad4..ef087afbd 100644 --- a/api/pkg/handlers/bulk_message_handler.go +++ b/api/pkg/handlers/bulk_message_handler.go @@ -67,7 +67,7 @@ func (h *BulkMessageHandler) Index(c fiber.Ctx) error { orders, err := h.messageService.GetBulkMessages(ctx, h.userIDFomContext(c)) if err != nil { - ctxLogger.Error(stacktrace.Propagate(err, "cannot fetch bulk messages for user [%s]", h.userIDFomContext(c))) + ctxLogger.Error(stacktrace.Propagatef(err, "cannot fetch bulk messages for user [%s]", h.userIDFomContext(c))) return h.responseInternalServerError(c) } @@ -94,18 +94,18 @@ func (h *BulkMessageHandler) Store(c fiber.Ctx) error { file, err := c.FormFile("document") if err != nil { - ctxLogger.Warn(stacktrace.Propagate(err, "cannot fetch file with name [%s] from request", "document")) + ctxLogger.Warn(stacktrace.Propagatef(err, "cannot fetch file with name [%s] from request", "document")) return h.responseBadRequest(c, err) } messages, userLocation, validationErrors := h.validator.ValidateStore(ctx, h.userIDFomContext(c), file) if len(validationErrors) != 0 { - ctxLogger.Warn(stacktrace.NewError("validation errors [%s], while sending bulk sms from CSV file [%s] for [%s]", spew.Sdump(validationErrors), file.Filename, h.userIDFomContext(c))) + ctxLogger.Warn(stacktrace.NewErrorf("validation errors [%s], while sending bulk sms from CSV file [%s] for [%s]", spew.Sdump(validationErrors), file.Filename, h.userIDFomContext(c))) return h.responseUnprocessableEntity(c, validationErrors, "validation errors while sending bulk SMS") } if msg := h.billingService.IsEntitledWithCount(ctx, h.userIDFomContext(c), uint(len(messages))); msg != nil { - ctxLogger.Warn(stacktrace.NewError("user with ID [%s] is not entitled to send [%d] messages", h.userIDFomContext(c), len(messages))) + ctxLogger.Warn(stacktrace.NewErrorf("user with ID [%s] is not entitled to send [%d] messages", h.userIDFomContext(c), len(messages))) return h.responsePaymentRequired(c, *msg) } @@ -133,7 +133,7 @@ func (h *BulkMessageHandler) Store(c fiber.Ctx) error { if err != nil { count.Add(-1) - ctxLogger.Error(stacktrace.Propagate(err, "cannot send message with payload [%s] at index [%d]", spew.Sdump(message), index)) + ctxLogger.Error(stacktrace.Propagatef(err, "cannot send message with payload [%s] at index [%d]", spew.Sdump(message), index)) } wg.Done() }(message, perPhoneIndex) diff --git a/api/pkg/handlers/discord_handler.go b/api/pkg/handlers/discord_handler.go index 582cdb325..6f7403f14 100644 --- a/api/pkg/handlers/discord_handler.go +++ b/api/pkg/handlers/discord_handler.go @@ -88,18 +88,18 @@ func (h *DiscordHandler) Index(c fiber.Ctx) error { var request requests.DiscordIndex if err := c.Bind().Query(&request); err != nil { - ctxLogger.Warn(stacktrace.Propagate(err, "cannot marshall URL [%s] into %T", c.OriginalURL(), request)) + ctxLogger.Warn(stacktrace.Propagatef(err, "cannot marshall URL [%s] into %T", c.OriginalURL(), request)) return h.responseBadRequest(c, err) } if errors := h.validator.ValidateIndex(ctx, request.Sanitize()); len(errors) != 0 { - ctxLogger.Warn(stacktrace.NewError("validation errors [%s], while fetching discord integrations [%+#v]", spew.Sdump(errors), request)) + ctxLogger.Warn(stacktrace.NewErrorf("validation errors [%s], while fetching discord integrations [%+#v]", spew.Sdump(errors), request)) return h.responseUnprocessableEntity(c, errors, "validation errors while fetching discord integrations") } discordIntegrations, err := h.service.Index(ctx, h.userIDFomContext(c), request.ToIndexParams()) if err != nil { - ctxLogger.Error(stacktrace.Propagate(err, "cannot get discord integrations with params [%+#v]", request)) + ctxLogger.Error(stacktrace.Propagatef(err, "cannot get discord integrations with params [%+#v]", request)) return h.responseInternalServerError(c) } @@ -126,13 +126,13 @@ func (h *DiscordHandler) Delete(c fiber.Ctx) error { discordID := c.Params("discordID") if errors := h.validator.ValidateUUID(discordID, "discordID"); len(errors) != 0 { - ctxLogger.Warn(stacktrace.NewError("validation errors [%s], while deleting discord integration with ID [%s]", spew.Sdump(errors), discordID)) + ctxLogger.Warn(stacktrace.NewErrorf("validation errors [%s], while deleting discord integration with ID [%s]", spew.Sdump(errors), discordID)) return h.responseUnprocessableEntity(c, errors, "validation errors while deleting discord integration") } err := h.service.Delete(ctx, h.userIDFomContext(c), uuid.MustParse(discordID)) if err != nil { - ctxLogger.Error(stacktrace.Propagate(err, "cannot delete discord integration with ID [%+#v]", discordID)) + ctxLogger.Error(stacktrace.Propagatef(err, "cannot delete discord integration with ID [%+#v]", discordID)) return h.responseInternalServerError(c) } @@ -160,19 +160,19 @@ func (h *DiscordHandler) Update(c fiber.Ctx) error { var request requests.DiscordUpdate if err := c.Bind().Body(&request); err != nil { - ctxLogger.Warn(stacktrace.Propagate(err, "cannot marshall params [%s] into [%T]", c.Body(), request)) + ctxLogger.Warn(stacktrace.Propagatef(err, "cannot marshall params [%s] into [%T]", c.Body(), request)) return h.responseBadRequest(c, err) } request.DiscordID = c.Params("discordID") if errors := h.validator.ValidateUpdate(ctx, request.Sanitize()); len(errors) != 0 { - ctxLogger.Warn(stacktrace.NewError("validation errors [%s], while updating user [%+#v]", spew.Sdump(errors), request)) + ctxLogger.Warn(stacktrace.NewErrorf("validation errors [%s], while updating user [%+#v]", spew.Sdump(errors), request)) return h.responseUnprocessableEntity(c, errors, "validation errors while updating discord integration") } user, err := h.service.Update(ctx, request.ToUpdateParams(h.userFromContext(c))) if err != nil { - ctxLogger.Error(stacktrace.Propagate(err, "cannot update discord integration with params [%+#v]", request)) + ctxLogger.Error(stacktrace.Propagatef(err, "cannot update discord integration with params [%+#v]", request)) return h.responseInternalServerError(c) } @@ -201,29 +201,29 @@ func (h *DiscordHandler) Store(c fiber.Ctx) error { var request requests.DiscordStore if err := c.Bind().Body(&request); err != nil { - ctxLogger.Warn(stacktrace.Propagate(err, "cannot marshall body [%s] into [%T]", c.Body(), request)) + ctxLogger.Warn(stacktrace.Propagatef(err, "cannot marshall body [%s] into [%T]", c.Body(), request)) return h.responseBadRequest(c, err) } if errors := h.validator.ValidateStore(ctx, request.Sanitize()); len(errors) != 0 { - ctxLogger.Warn(stacktrace.NewError("validation errors [%s], while storing discord integration [%+#v]", spew.Sdump(errors), request)) + ctxLogger.Warn(stacktrace.NewErrorf("validation errors [%s], while storing discord integration [%+#v]", spew.Sdump(errors), request)) return h.responseUnprocessableEntity(c, errors, "validation errors while storing discord integration") } discordIntegrations, err := h.service.Index(ctx, h.userIDFomContext(c), repositories.IndexParams{Skip: 0, Limit: 1}) if err != nil { - ctxLogger.Error(stacktrace.Propagate(err, "cannot index discord integrations for user [%s]", h.userIDFomContext(c))) + ctxLogger.Error(stacktrace.Propagatef(err, "cannot index discord integrations for user [%s]", h.userIDFomContext(c))) return h.responseInternalServerError(c) } if len(discordIntegrations) > 0 { - ctxLogger.Warn(stacktrace.NewError("user with ID [%s] wants to create more than 1 discord integration", h.userIDFomContext(c))) + ctxLogger.Warn(stacktrace.NewErrorf("user with ID [%s] wants to create more than 1 discord integration", h.userIDFomContext(c))) return h.responsePaymentRequired(c, "You can't create more than 1 discord integration contact us to upgrade your account.") } discordIntegration, err := h.service.Store(ctx, request.ToStoreParams(h.userFromContext(c))) if err != nil { - ctxLogger.Error(stacktrace.Propagate(err, "cannot store discord integration with params [%+#v]", request)) + ctxLogger.Error(stacktrace.Propagatef(err, "cannot store discord integration with params [%+#v]", request)) return h.responseInternalServerError(c) } @@ -252,7 +252,7 @@ func (h *DiscordHandler) Event(c fiber.Ctx) error { var payload map[string]any if err := json.Unmarshal(c.Body(), &payload); err != nil { - ctxLogger.Error(h.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot unmarshall [%s] to [%T]", string(c.Body()), payload))) + ctxLogger.Error(h.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot unmarshall [%s] to [%T]", string(c.Body()), payload))) return h.responseBadRequest(c, err) } @@ -266,7 +266,7 @@ func (h *DiscordHandler) Event(c fiber.Ctx) error { return h.sendSMS(ctx, c, payload) } - return h.responseBadRequest(c, stacktrace.NewError("unknown type [%d]", payload["type"])) + return h.responseBadRequest(c, stacktrace.NewErrorf("unknown type [%d]", payload["type"])) } func (h *DiscordHandler) createRequest(payload map[string]any) requests.MessageSend { @@ -291,7 +291,7 @@ func (h *DiscordHandler) sendSMS(ctx context.Context, c fiber.Ctx, payload map[s discord, err := h.service.GetByServerID(ctx, payload["guild_id"].(string)) if err != nil { - ctxLogger.Error(h.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot get discord integration by server ID [%s]", payload["guild_id"].(string)))) + ctxLogger.Error(h.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot get discord integration by server ID [%s]", payload["guild_id"].(string)))) return c.JSON( fiber.Map{ "type": 4, @@ -329,7 +329,7 @@ func (h *DiscordHandler) sendSMS(ctx context.Context, c fiber.Ctx, payload map[s } if errors := h.messageValidator.ValidateMessageSend(ctx, discord.UserID, request.Sanitize()); len(errors) != 0 { - ctxLogger.Warn(stacktrace.NewError("validation errors [%s], while sending payload [%s]", spew.Sdump(errors), c.Body())) + ctxLogger.Warn(stacktrace.NewErrorf("validation errors [%s], while sending payload [%s]", spew.Sdump(errors), c.Body())) var embeds []fiber.Map for _, value := range errors { @@ -351,7 +351,7 @@ func (h *DiscordHandler) sendSMS(ctx context.Context, c fiber.Ctx, payload map[s } if msg := h.billingService.IsEntitled(ctx, discord.UserID); msg != nil { - ctxLogger.Warn(stacktrace.NewError("user with ID [%s] can't send a message", discord.UserID)) + ctxLogger.Warn(stacktrace.NewErrorf("user with ID [%s] can't send a message", discord.UserID)) return c.JSON( fiber.Map{ "type": 4, @@ -370,7 +370,7 @@ func (h *DiscordHandler) sendSMS(ctx context.Context, c fiber.Ctx, payload map[s message, err := h.messageService.SendMessage(ctx, request.ToMessageSendParams(discord.UserID, c.OriginalURL())) if err != nil { - ctxLogger.Error(stacktrace.Propagate(err, "cannot send message with paylod [%s] from discord server [%s]", c.Body(), discord.ServerID)) + ctxLogger.Error(stacktrace.Propagatef(err, "cannot send message with paylod [%s] from discord server [%s]", c.Body(), discord.ServerID)) return c.JSON( fiber.Map{ "type": 4, @@ -436,7 +436,7 @@ func (h *DiscordHandler) verifyInteraction(ctxLogger telemetry.Logger, c fiber.C key, err := hex.DecodeString(os.Getenv("DISCORD_PUBLIC_KEY")) if err != nil { - ctxLogger.Error(stacktrace.Propagate(err, "cannot decode DISCORD_PUBLIC_KEY env variable [%s]", os.Getenv("DISCORD_PUBLIC_KEY"))) + ctxLogger.Error(stacktrace.Propagatef(err, "cannot decode DISCORD_PUBLIC_KEY env variable [%s]", os.Getenv("DISCORD_PUBLIC_KEY"))) return false } diff --git a/api/pkg/handlers/events_handler.go b/api/pkg/handlers/events_handler.go index 5d4fc121b..5e856cc70 100644 --- a/api/pkg/handlers/events_handler.go +++ b/api/pkg/handlers/events_handler.go @@ -49,24 +49,24 @@ func (h *EventsHandler) Dispatch(c fiber.Ctx) error { var request cloudevents.Event if err := c.Bind().Body(&request); err != nil { - ctxLogger.Warn(stacktrace.Propagate(err, "cannot marshall params [%s] into %T", c.OriginalURL(), request)) + ctxLogger.Warn(stacktrace.Propagatef(err, "cannot marshall params [%s] into %T", c.OriginalURL(), request)) return h.responseBadRequest(c, err) } if err := request.Validate(); err != nil { - ctxLogger.Warn(stacktrace.NewError("validation errors [%s], while dispatching event [%+#v]", spew.Sdump(err.Error()), request)) + ctxLogger.Warn(stacktrace.NewErrorf("validation errors [%s], while dispatching event [%+#v]", spew.Sdump(err.Error()), request)) return h.responseUnprocessableEntity(c, map[string][]string{"event": {err.Error()}}, "validation errors while dispatching event") } if h.userIDFomContext(c) != h.queueConfig.UserID { - ctxLogger.Error(stacktrace.NewError("user with ID [%s], cannot dispatch event [%+#v]", h.userIDFomContext(c), request)) + ctxLogger.Error(stacktrace.NewErrorf("user with ID [%s], cannot dispatch event [%+#v]", h.userIDFomContext(c), request)) return h.responseForbidden(c) } ctxLogger.Info(fmt.Sprintf("handling [%s] event with ID [%s]", request.Type(), request.ID())) err := h.service.DispatchSync(ctx, request) if err != nil { - ctxLogger.Error(stacktrace.Propagate(err, "cannot dispatch [%s] event with ID [%s]", request.Type(), request.ID())) + ctxLogger.Error(stacktrace.Propagatef(err, "cannot dispatch [%s] event with ID [%s]", request.Type(), request.ID())) return h.responseInternalServerError(c) } diff --git a/api/pkg/handlers/heartbeat_handler.go b/api/pkg/handlers/heartbeat_handler.go index 663d16af3..660bdeed4 100644 --- a/api/pkg/handlers/heartbeat_handler.go +++ b/api/pkg/handlers/heartbeat_handler.go @@ -74,18 +74,18 @@ func (h *HeartbeatHandler) Index(c fiber.Ctx) error { var request requests.HeartbeatIndex if err := c.Bind().Query(&request); err != nil { - ctxLogger.Warn(stacktrace.Propagate(err, "cannot marshall params [%s] into %T", c.OriginalURL(), request)) + ctxLogger.Warn(stacktrace.Propagatef(err, "cannot marshall params [%s] into %T", c.OriginalURL(), request)) return h.responseBadRequest(c, err) } if errors := h.validator.ValidateIndex(ctx, request.Sanitize()); len(errors) != 0 { - ctxLogger.Warn(stacktrace.NewError("validation errors [%s], while fetching heartbeats [%+#v]", spew.Sdump(errors), request)) + ctxLogger.Warn(stacktrace.NewErrorf("validation errors [%s], while fetching heartbeats [%+#v]", spew.Sdump(errors), request)) return h.responseUnprocessableEntity(c, errors, "validation errors while fetching heartbeats") } heartbeats, err := h.service.Index(ctx, h.userIDFomContext(c), request.Owner, request.ToIndexParams()) if err != nil { - ctxLogger.Error(stacktrace.Propagate(err, "cannot get messgaes with params [%+#v]", request)) + ctxLogger.Error(stacktrace.Propagatef(err, "cannot get messgaes with params [%+#v]", request)) return h.responseInternalServerError(c) } @@ -114,18 +114,18 @@ func (h *HeartbeatHandler) Store(c fiber.Ctx) error { var request requests.HeartbeatStore if err := c.Bind().Body(&request); err != nil { - ctxLogger.Warn(stacktrace.Propagate(err, "cannot marshall params [%s] into %T", c.OriginalURL(), request)) + ctxLogger.Warn(stacktrace.Propagatef(err, "cannot marshall params [%s] into %T", c.OriginalURL(), request)) return h.responseBadRequest(c, err) } if errors := h.validator.ValidateStore(ctx, request.Sanitize()); len(errors) != 0 { - ctxLogger.Warn(stacktrace.NewError("validation errors [%s], while storing heartbeat [%+#v]", spew.Sdump(errors), request)) + ctxLogger.Warn(stacktrace.NewErrorf("validation errors [%s], while storing heartbeat [%+#v]", spew.Sdump(errors), request)) return h.responseUnprocessableEntity(c, errors, "validation errors while storing heartbeat") } for _, phoneNumber := range request.PhoneNumbers { if !h.authorizePhoneAPIKey(c, phoneNumber) { - ctxLogger.Warn(stacktrace.NewError("phone API Key ID [%s] is not authorized to store heartbeat for phone number [%s]", h.userFromContext(c).PhoneAPIKeyID, phoneNumber)) + ctxLogger.Warn(stacktrace.NewErrorf("phone API Key ID [%s] is not authorized to store heartbeat for phone number [%s]", h.userFromContext(c).PhoneAPIKeyID, phoneNumber)) return h.responsePhoneAPIKeyUnauthorized(c, phoneNumber, h.userFromContext(c)) } } @@ -139,7 +139,7 @@ func (h *HeartbeatHandler) Store(c fiber.Ctx) error { go func(input services.HeartbeatStoreParams, index int) { response, err := h.service.Store(ctx, input) if err != nil { - ctxLogger.Error(stacktrace.Propagate(err, "cannot store heartbeat with params [%+#v]", request)) + ctxLogger.Error(stacktrace.Propagatef(err, "cannot store heartbeat with params [%+#v]", request)) } responses[index] = response wg.Done() diff --git a/api/pkg/handlers/integration_3cx_handler.go b/api/pkg/handlers/integration_3cx_handler.go index db06e64df..63c88bffe 100644 --- a/api/pkg/handlers/integration_3cx_handler.go +++ b/api/pkg/handlers/integration_3cx_handler.go @@ -62,19 +62,19 @@ func (h *Integration3CXHandler) Messages(c fiber.Ctx) error { var request requests.Integration3CXMessage if err := c.Bind().Body(&request); err != nil { - ctxLogger.Warn(stacktrace.Propagate(err, "cannot marshall [%s] into %T", c.Body(), request)) + ctxLogger.Warn(stacktrace.Propagatef(err, "cannot marshall [%s] into %T", c.Body(), request)) return h.responseBadRequest(c, err) } if msg := h.billingService.IsEntitled(ctx, h.userIDFomContext(c)); msg != nil { - ctxLogger.Warn(stacktrace.NewError("user with ID [%s] can't send a [3cx] message", h.userIDFomContext(c))) + ctxLogger.Warn(stacktrace.NewErrorf("user with ID [%s] can't send a [3cx] message", h.userIDFomContext(c))) return h.responsePaymentRequired(c, *msg) } request.Sanitize() message, err := h.messageService.SendMessage(ctx, request.ToMessageSendParams(h.userIDFomContext(c), c.OriginalURL())) if err != nil { - ctxLogger.Error(stacktrace.Propagate(err, "cannot send [3cx] message with payload [%s]", c.Body())) + ctxLogger.Error(stacktrace.Propagatef(err, "cannot send [3cx] message with payload [%s]", c.Body())) return h.responseInternalServerError(c) } diff --git a/api/pkg/handlers/lemonsqueezy_handler.go b/api/pkg/handlers/lemonsqueezy_handler.go index 1b18d8baa..9147cd311 100644 --- a/api/pkg/handlers/lemonsqueezy_handler.go +++ b/api/pkg/handlers/lemonsqueezy_handler.go @@ -51,12 +51,12 @@ func (h *LemonsqueezyHandler) Event(c fiber.Ctx) error { signature := c.Get("X-Signature") if errors := h.validator.ValidateEvent(ctx, signature, c.Body()); len(errors) != 0 { - ctxLogger.Warn(stacktrace.NewError("validation errors [%s], while storing request [%s] and signature [%s]", spew.Sdump(errors), c.Body(), signature)) + ctxLogger.Warn(stacktrace.NewErrorf("validation errors [%s], while storing request [%s] and signature [%s]", spew.Sdump(errors), c.Body(), signature)) return h.responseUnprocessableEntity(c, errors, "validation errors while storing lemonsqueezy event") } if err := h.handleRequest(ctx, c); err != nil { - ctxLogger.Error(stacktrace.Propagate(err, "cannot handle lemonsqueezy event [%s]", c.Body())) + ctxLogger.Error(stacktrace.Propagatef(err, "cannot handle lemonsqueezy event [%s]", c.Body())) return h.responseInternalServerError(c) } @@ -70,31 +70,31 @@ func (h *LemonsqueezyHandler) handleRequest(ctx context.Context, c fiber.Ctx) er var request lemonsqueezy.WebhookRequestSubscription err := json.Unmarshal(c.Body(), &request) if err != nil { - return stacktrace.Propagate(err, "cannot marshall [%s] to [%T]", c.Body(), request) + return stacktrace.Propagatef(err, "cannot marshall [%s] to [%T]", c.Body(), request) } return h.service.HandleSubscriptionCreatedEvent(ctx, c.OriginalURL(), &request) case "subscription_cancelled": var request lemonsqueezy.WebhookRequestSubscription err := json.Unmarshal(c.Body(), &request) if err != nil { - return stacktrace.Propagate(err, "cannot marshall [%s] to [%T]", c.Body(), request) + return stacktrace.Propagatef(err, "cannot marshall [%s] to [%T]", c.Body(), request) } return h.service.HandleSubscriptionCanceledEvent(ctx, c.OriginalURL(), &request) case "subscription_expired": var request lemonsqueezy.WebhookRequestSubscription err := json.Unmarshal(c.Body(), &request) if err != nil { - return stacktrace.Propagate(err, "cannot marshall [%s] to [%T]", c.Body(), request) + return stacktrace.Propagatef(err, "cannot marshall [%s] to [%T]", c.Body(), request) } return h.service.HandleSubscriptionExpiredEvent(ctx, c.OriginalURL(), &request) case "subscription_updated": var request lemonsqueezy.WebhookRequestSubscription err := json.Unmarshal(c.Body(), &request) if err != nil { - return stacktrace.Propagate(err, "cannot marshall [%s] to [%T]", c.Body(), request) + return stacktrace.Propagatef(err, "cannot marshall [%s] to [%T]", c.Body(), request) } return h.service.HandleSubscriptionUpdatedEvent(ctx, c.OriginalURL(), &request) default: - return stacktrace.NewError("invalid event [%s] received with request [%s]", eventName, c.Body()) + return stacktrace.NewErrorf("invalid event [%s] received with request [%s]", eventName, c.Body()) } } diff --git a/api/pkg/handlers/message_handler.go b/api/pkg/handlers/message_handler.go index 14a4e95e5..8c7a1420a 100644 --- a/api/pkg/handlers/message_handler.go +++ b/api/pkg/handlers/message_handler.go @@ -88,23 +88,23 @@ func (h *MessageHandler) PostSend(c fiber.Ctx) error { var request requests.MessageSend if err := c.Bind().Body(&request); err != nil { - ctxLogger.Warn(stacktrace.Propagate(err, "cannot marshall [%s] into %T", c.Body(), request)) + ctxLogger.Warn(stacktrace.Propagatef(err, "cannot marshall [%s] into %T", c.Body(), request)) return h.responseBadRequest(c, err) } if errors := h.validator.ValidateMessageSend(ctx, h.userIDFomContext(c), request.Sanitize()); len(errors) != 0 { - ctxLogger.Warn(stacktrace.NewError("validation errors [%s], while sending payload [%s]", spew.Sdump(errors), c.Body())) + ctxLogger.Warn(stacktrace.NewErrorf("validation errors [%s], while sending payload [%s]", spew.Sdump(errors), c.Body())) return h.responseUnprocessableEntity(c, errors, "validation errors while sending message") } if msg := h.billingService.IsEntitled(ctx, h.userIDFomContext(c)); msg != nil { - ctxLogger.Warn(stacktrace.NewError("user with ID [%s] can't send a message", h.userIDFomContext(c))) + ctxLogger.Warn(stacktrace.NewErrorf("user with ID [%s] can't send a message", h.userIDFomContext(c))) return h.responsePaymentRequired(c, *msg) } message, err := h.service.SendMessage(ctx, request.ToMessageSendParams(h.userIDFomContext(c), c.OriginalURL())) if err != nil { - ctxLogger.Error(stacktrace.Propagate(err, "cannot send message with paylod [%s]", c.Body())) + ctxLogger.Error(stacktrace.Propagatef(err, "cannot send message with paylod [%s]", c.Body())) return h.responseInternalServerError(c) } @@ -133,17 +133,17 @@ func (h *MessageHandler) BulkSend(c fiber.Ctx) error { var request requests.MessageBulkSend if err := c.Bind().Body(&request); err != nil { - ctxLogger.Warn(stacktrace.Propagate(err, "cannot marshall [%s] into %T", c.Body(), request)) + ctxLogger.Warn(stacktrace.Propagatef(err, "cannot marshall [%s] into %T", c.Body(), request)) return h.responseBadRequest(c, err) } if errors := h.validator.ValidateMessageBulkSend(ctx, h.userIDFomContext(c), request.Sanitize()); len(errors) != 0 { - ctxLogger.Warn(stacktrace.NewError("validation errors [%s], while sending payload [%s]", spew.Sdump(errors), c.Body())) + ctxLogger.Warn(stacktrace.NewErrorf("validation errors [%s], while sending payload [%s]", spew.Sdump(errors), c.Body())) return h.responseUnprocessableEntity(c, errors, "validation errors while sending messages") } if msg := h.billingService.IsEntitledWithCount(ctx, h.userIDFomContext(c), uint(len(request.To))); msg != nil { - ctxLogger.Warn(stacktrace.NewError("user with ID [%s] is not entitled to send [%d] messages", h.userIDFomContext(c), len(request.To))) + ctxLogger.Warn(stacktrace.NewErrorf("user with ID [%s] is not entitled to send [%d] messages", h.userIDFomContext(c), len(request.To))) return h.responsePaymentRequired(c, *msg) } @@ -160,7 +160,7 @@ func (h *MessageHandler) BulkSend(c fiber.Ctx) error { if err != nil { count.Add(-1) - ctxLogger.Error(stacktrace.Propagate(err, "cannot send message with paylod [%s] at index [%d]", spew.Sdump(message), index)) + ctxLogger.Error(stacktrace.Propagatef(err, "cannot send message with paylod [%s] at index [%d]", spew.Sdump(message), index)) } responses[index] = response wg.Done() @@ -194,24 +194,24 @@ func (h *MessageHandler) GetOutstanding(c fiber.Ctx) error { var request requests.MessageOutstanding if err := c.Bind().Query(&request); err != nil { - ctxLogger.Warn(stacktrace.Propagate(err, "cannot marshall params [%s] into %T", c.OriginalURL(), request)) + ctxLogger.Warn(stacktrace.Propagatef(err, "cannot marshall params [%s] into %T", c.OriginalURL(), request)) return h.responseBadRequest(c, err) } if errors := h.validator.ValidateMessageOutstanding(ctx, request.Sanitize()); len(errors) != 0 { - ctxLogger.Warn(stacktrace.NewError("validation errors [%s], while fetching outstanding messages [%s]", spew.Sdump(errors), c.OriginalURL())) + ctxLogger.Warn(stacktrace.NewErrorf("validation errors [%s], while fetching outstanding messages [%s]", spew.Sdump(errors), c.OriginalURL())) return h.responseUnprocessableEntity(c, errors, "validation errors while fetching outstanding messages") } message, err := h.service.GetOutstanding(ctx, request.ToGetOutstandingParams(c.Path(), h.userFromContext(c), timestamp)) if stacktrace.GetCode(err) == repositories.ErrCodeNotFound { msg := fmt.Sprintf("Cannot find outstanding message with ID [%s]", request.MessageID) - ctxLogger.Warn(stacktrace.Propagate(err, "Cannot find outstanding message with ID [%s]", request.MessageID)) + ctxLogger.Warn(stacktrace.Propagatef(err, "Cannot find outstanding message with ID [%s]", request.MessageID)) return h.responseNotFound(c, msg) } if err != nil { - ctxLogger.Error(stacktrace.Propagate(err, "cannot get outstanding messgage with ID [%s]", request.MessageID)) + ctxLogger.Error(stacktrace.Propagatef(err, "cannot get outstanding messgage with ID [%s]", request.MessageID)) return h.responseInternalServerError(c) } @@ -244,18 +244,18 @@ func (h *MessageHandler) Index(c fiber.Ctx) error { var request requests.MessageIndex if err := c.Bind().Query(&request); err != nil { - ctxLogger.Warn(stacktrace.Propagate(err, "cannot marshall params [%s] into %T", c.OriginalURL(), request)) + ctxLogger.Warn(stacktrace.Propagatef(err, "cannot marshall params [%s] into %T", c.OriginalURL(), request)) return h.responseBadRequest(c, err) } if errors := h.validator.ValidateMessageIndex(ctx, request.Sanitize()); len(errors) != 0 { - ctxLogger.Warn(stacktrace.NewError("validation errors [%s], while fetching messages [%+#v]", spew.Sdump(errors), request)) + ctxLogger.Warn(stacktrace.NewErrorf("validation errors [%s], while fetching messages [%+#v]", spew.Sdump(errors), request)) return h.responseUnprocessableEntity(c, errors, "validation errors while fetching messages") } messages, err := h.service.GetMessages(ctx, request.ToGetParams(h.userIDFomContext(c))) if err != nil { - ctxLogger.Error(stacktrace.Propagate(err, "cannot get messages with params [%+#v]", request)) + ctxLogger.Error(stacktrace.Propagatef(err, "cannot get messages with params [%+#v]", request)) return h.responseInternalServerError(c) } @@ -286,7 +286,7 @@ func (h *MessageHandler) PostEvent(c fiber.Ctx) error { var request requests.MessageEvent if err := c.Bind().Body(&request); err != nil { - ctxLogger.Warn(stacktrace.Propagate(err, "cannot marshall [%s] into %T", c.Body(), request)) + ctxLogger.Warn(stacktrace.Propagatef(err, "cannot marshall [%s] into %T", c.Body(), request)) return h.responseBadRequest(c, err) } @@ -296,7 +296,7 @@ func (h *MessageHandler) PostEvent(c fiber.Ctx) error { } if errors := h.validator.ValidateMessageEvent(ctx, request.Sanitize()); len(errors) != 0 { - ctxLogger.Warn(stacktrace.NewError("validation errors [%s], while storing event [%s] for message [%s]", spew.Sdump(errors), c.Body(), request.MessageID)) + ctxLogger.Warn(stacktrace.NewErrorf("validation errors [%s], while storing event [%s] for message [%s]", spew.Sdump(errors), c.Body(), request.MessageID)) return h.responseUnprocessableEntity(c, errors, "validation errors while storing event") } @@ -306,18 +306,18 @@ func (h *MessageHandler) PostEvent(c fiber.Ctx) error { } if err != nil { - ctxLogger.Error(h.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot find message with id [%s]", request.MessageID))) + ctxLogger.Error(h.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot find message with id [%s]", request.MessageID))) return h.responseInternalServerError(c) } if !h.authorizePhoneAPIKey(c, message.Owner) { - ctxLogger.Warn(stacktrace.NewError("user with ID [%s] is not authorized to send event for message with ID [%s]", h.userIDFomContext(c), request.MessageID)) + ctxLogger.Warn(stacktrace.NewErrorf("user with ID [%s] is not authorized to send event for message with ID [%s]", h.userIDFomContext(c), request.MessageID)) return h.responsePhoneAPIKeyUnauthorized(c, message.Owner, h.userFromContext(c)) } message, err = h.service.StoreEvent(ctx, message, request.ToMessageStoreEventParams(c.OriginalURL())) if err != nil { - ctxLogger.Error(h.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot store event for message [%s] with paylod [%s]", request.MessageID, c.Body()))) + ctxLogger.Error(h.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot store event for message [%s] with paylod [%s]", request.MessageID, c.Body()))) return h.responseInternalServerError(c) } @@ -345,28 +345,28 @@ func (h *MessageHandler) PostReceive(c fiber.Ctx) error { var request requests.MessageReceive if err := c.Bind().Body(&request); err != nil { - ctxLogger.Warn(stacktrace.Propagate(err, "cannot marshall [%s] into %T", c.Body(), request)) + ctxLogger.Warn(stacktrace.Propagatef(err, "cannot marshall [%s] into %T", c.Body(), request)) return h.responseBadRequest(c, err) } if errors := h.validator.ValidateMessageReceive(ctx, request.Sanitize()); len(errors) != 0 { - ctxLogger.Warn(stacktrace.NewError("validation errors [%s], while sending payload [%s]", spew.Sdump(errors), c.Body())) + ctxLogger.Warn(stacktrace.NewErrorf("validation errors [%s], while sending payload [%s]", spew.Sdump(errors), c.Body())) return h.responseUnprocessableEntity(c, errors, "validation errors while receiving message") } if msg := h.billingService.IsEntitled(ctx, h.userIDFomContext(c)); msg != nil { - ctxLogger.Warn(stacktrace.NewError("user with ID [%s] can't receive a message becasuse they have exceeded the limit", h.userIDFomContext(c))) + ctxLogger.Warn(stacktrace.NewErrorf("user with ID [%s] can't receive a message becasuse they have exceeded the limit", h.userIDFomContext(c))) return h.responsePaymentRequired(c, *msg) } if !h.authorizePhoneAPIKey(c, request.To) { - ctxLogger.Warn(stacktrace.NewError("user with ID [%s] is not authorized to receive message to phone number [%s]", h.userIDFomContext(c), request.To)) + ctxLogger.Warn(stacktrace.NewErrorf("user with ID [%s] is not authorized to receive message to phone number [%s]", h.userIDFomContext(c), request.To)) return h.responsePhoneAPIKeyUnauthorized(c, request.To, h.userFromContext(c)) } message, err := h.service.ReceiveMessage(ctx, request.ToMessageReceiveParams(h.userIDFomContext(c), c.OriginalURL())) if err != nil { - ctxLogger.Error(stacktrace.Propagate(err, "cannot receive message with payload [%s]", c.Body())) + ctxLogger.Error(stacktrace.Propagatef(err, "cannot receive message with payload [%s]", c.Body())) return h.responseInternalServerError(c) } @@ -396,7 +396,7 @@ func (h *MessageHandler) Delete(c fiber.Ctx) error { messageID := c.Params("messageID") if errors := h.validator.ValidateUUID(messageID, "messageID"); len(errors) != 0 { - ctxLogger.Warn(stacktrace.NewError("validation errors [%s], while deleting a message with ID [%s]", spew.Sdump(errors), messageID)) + ctxLogger.Warn(stacktrace.NewErrorf("validation errors [%s], while deleting a message with ID [%s]", spew.Sdump(errors), messageID)) return h.responseUnprocessableEntity(c, errors, "validation errors while storing event") } @@ -406,12 +406,12 @@ func (h *MessageHandler) Delete(c fiber.Ctx) error { } if err != nil { - ctxLogger.Error(h.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot find message with id [%s]", messageID))) + ctxLogger.Error(h.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot find message with id [%s]", messageID))) return h.responseInternalServerError(c) } if err = h.service.DeleteMessage(ctx, c.OriginalURL(), message); err != nil { - ctxLogger.Error(h.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot delete message with ID [%s] for user with ID [%s]", messageID, message.UserID))) + ctxLogger.Error(h.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot delete message with ID [%s] for user with ID [%s]", messageID, message.UserID))) return h.responseInternalServerError(c) } @@ -441,7 +441,7 @@ func (h *MessageHandler) Get(c fiber.Ctx) error { messageID := c.Params("messageID") if errors := h.validator.ValidateUUID(messageID, "messageID"); len(errors) != 0 { - ctxLogger.Warn(stacktrace.NewError("validation errors [%s], while deleting a message with ID [%s]", spew.Sdump(errors), messageID)) + ctxLogger.Warn(stacktrace.NewErrorf("validation errors [%s], while deleting a message with ID [%s]", spew.Sdump(errors), messageID)) return h.responseUnprocessableEntity(c, errors, "validation errors while storing event") } @@ -451,7 +451,7 @@ func (h *MessageHandler) Get(c fiber.Ctx) error { } if err != nil { - ctxLogger.Error(h.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot find message with id [%s]", messageID))) + ctxLogger.Error(h.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot find message with id [%s]", messageID))) return h.responseInternalServerError(c) } @@ -481,23 +481,23 @@ func (h *MessageHandler) PostCallMissed(c fiber.Ctx) error { var request requests.MessageCallMissed if err := c.Bind().Body(&request); err != nil { - ctxLogger.Warn(stacktrace.Propagate(err, "cannot marshall [%s] into %T", c.Body(), request)) + ctxLogger.Warn(stacktrace.Propagatef(err, "cannot marshall [%s] into %T", c.Body(), request)) return h.responseBadRequest(c, err) } if errors := h.validator.ValidateCallMissed(ctx, request.Sanitize()); len(errors) != 0 { - ctxLogger.Warn(stacktrace.NewError("validation errors [%s], for missed call event [%s]", spew.Sdump(errors), c.Body())) + ctxLogger.Warn(stacktrace.NewErrorf("validation errors [%s], for missed call event [%s]", spew.Sdump(errors), c.Body())) return h.responseUnprocessableEntity(c, errors, "validation errors while storing missed call event") } if !h.authorizePhoneAPIKey(c, request.To) { - ctxLogger.Warn(stacktrace.NewError("user with ID [%s] is not authorized to register missed phone call for phone number [%s]", h.userIDFomContext(c), request.To)) + ctxLogger.Warn(stacktrace.NewErrorf("user with ID [%s] is not authorized to register missed phone call for phone number [%s]", h.userIDFomContext(c), request.To)) return h.responsePhoneAPIKeyUnauthorized(c, request.To, h.userFromContext(c)) } message, err := h.service.RegisterMissedCall(ctx, request.ToCallMissedParams(h.userIDFomContext(c), c.OriginalURL())) if err != nil { - ctxLogger.Error(h.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot store missed call event for user [%s] with paylod [%s]", h.userIDFomContext(c), c.Body()))) + ctxLogger.Error(h.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot store missed call event for user [%s] with paylod [%s]", h.userIDFomContext(c), c.Body()))) return h.responseInternalServerError(c) } @@ -528,7 +528,7 @@ func (h *MessageHandler) Search(c fiber.Ctx) error { var request requests.MessageSearch if err := c.Bind().Query(&request); err != nil { - ctxLogger.Warn(stacktrace.Propagate(err, "cannot marshall params in [%s] into [%T]", c.OriginalURL(), request)) + ctxLogger.Warn(stacktrace.Propagatef(err, "cannot marshall params in [%s] into [%T]", c.OriginalURL(), request)) return h.responseBadRequest(c, err) } @@ -536,13 +536,13 @@ func (h *MessageHandler) Search(c fiber.Ctx) error { request.Token = c.Get("token") if errors := h.validator.ValidateMessageSearch(ctx, request.Sanitize()); len(errors) != 0 { - ctxLogger.Warn(stacktrace.NewError("validation errors [%s], while searching messages [%+#v]", spew.Sdump(errors), request)) + ctxLogger.Warn(stacktrace.NewErrorf("validation errors [%s], while searching messages [%+#v]", spew.Sdump(errors), request)) return h.responseUnprocessableEntity(c, errors, "validation errors while searching messages") } messages, err := h.service.SearchMessages(ctx, request.ToSearchParams(h.userIDFomContext(c))) if err != nil { - ctxLogger.Error(stacktrace.Propagate(err, "cannot search messages with params [%+#v]", request)) + ctxLogger.Error(stacktrace.Propagatef(err, "cannot search messages with params [%+#v]", request)) return h.responseInternalServerError(c) } diff --git a/api/pkg/handlers/message_send_schedule_handler.go b/api/pkg/handlers/message_send_schedule_handler.go index 93fac9480..1847bb713 100644 --- a/api/pkg/handlers/message_send_schedule_handler.go +++ b/api/pkg/handlers/message_send_schedule_handler.go @@ -69,7 +69,7 @@ func (h *MessageSendScheduleHandler) Index(c fiber.Ctx) error { schedules, err := h.service.Index(ctx, userID) if err != nil { - ctxLogger.Error(stacktrace.Propagate(err, "cannot list send schedules for user [%s]", userID)) + ctxLogger.Error(stacktrace.Propagatef(err, "cannot list send schedules for user [%s]", userID)) return h.responseInternalServerError(c) } @@ -102,7 +102,7 @@ func (h *MessageSendScheduleHandler) Store(c fiber.Ctx) error { return h.service.CountByUser(ctx, userID) }) if err != nil { - ctxLogger.Error(stacktrace.Propagate(err, "cannot check entitlement for send schedules for user [%s]", userID)) + ctxLogger.Error(stacktrace.Propagatef(err, "cannot check entitlement for send schedules for user [%s]", userID)) return h.responseInternalServerError(c) } if !result.Allowed { @@ -116,7 +116,7 @@ func (h *MessageSendScheduleHandler) Store(c fiber.Ctx) error { request = request.Sanitize() if errors := h.validator.ValidateStore(ctx, request); len(errors) != 0 { - ctxLogger.Warn(stacktrace.NewError( + ctxLogger.Warn(stacktrace.NewErrorf( "validation errors [%s], while storing send schedule [%+#v]", spew.Sdump(errors), request, @@ -126,7 +126,7 @@ func (h *MessageSendScheduleHandler) Store(c fiber.Ctx) error { schedule, err := h.service.Store(ctx, request.ToParams(h.userFromContext(c))) if err != nil { - ctxLogger.Error(stacktrace.Propagate(err, "cannot create send schedule for user [%s]", userID)) + ctxLogger.Error(stacktrace.Propagatef(err, "cannot create send schedule for user [%s]", userID)) return h.responseInternalServerError(c) } @@ -173,7 +173,7 @@ func (h *MessageSendScheduleHandler) Update(c fiber.Ctx) error { schedule, err := h.service.Update(ctx, userID, scheduleID, request.ToParams(h.userFromContext(c))) if err != nil { - ctxLogger.Error(stacktrace.Propagate(err, "cannot update send schedule for user [%s] and schedule [%s]", userID, scheduleID)) + ctxLogger.Error(stacktrace.Propagatef(err, "cannot update send schedule for user [%s] and schedule [%s]", userID, scheduleID)) if stacktrace.GetCode(err) == repositories.ErrCodeNotFound { return h.responseNotFound(c, err.Error()) } @@ -209,7 +209,7 @@ func (h *MessageSendScheduleHandler) Delete(c fiber.Ctx) error { userID := h.userIDFomContext(c) if _, err = h.service.Load(ctx, userID, scheduleID); err != nil { - ctxLogger.Error(stacktrace.Propagate(err, "cannot load send schedule for deletion for user [%s] and schedule [%s]", userID, scheduleID)) + ctxLogger.Error(stacktrace.Propagatef(err, "cannot load send schedule for deletion for user [%s] and schedule [%s]", userID, scheduleID)) if stacktrace.GetCode(err) == repositories.ErrCodeNotFound { return h.responseNotFound(c, err.Error()) } @@ -217,7 +217,7 @@ func (h *MessageSendScheduleHandler) Delete(c fiber.Ctx) error { } if err = h.service.Delete(ctx, userID, scheduleID); err != nil { - ctxLogger.Error(stacktrace.Propagate(err, "cannot delete send schedule for user [%s] and schedule [%s]", userID, scheduleID)) + ctxLogger.Error(stacktrace.Propagatef(err, "cannot delete send schedule for user [%s] and schedule [%s]", userID, scheduleID)) return h.responseInternalServerError(c) } diff --git a/api/pkg/handlers/message_thread_handler.go b/api/pkg/handlers/message_thread_handler.go index 996ca69be..a931302dc 100644 --- a/api/pkg/handlers/message_thread_handler.go +++ b/api/pkg/handlers/message_thread_handler.go @@ -73,18 +73,18 @@ func (h *MessageThreadHandler) Index(c fiber.Ctx) error { var request requests.MessageThreadIndex if err := c.Bind().Query(&request); err != nil { - ctxLogger.Warn(stacktrace.Propagate(err, "cannot marshall params [%s] into %T", c.OriginalURL(), request)) + ctxLogger.Warn(stacktrace.Propagatef(err, "cannot marshall params [%s] into %T", c.OriginalURL(), request)) return h.responseBadRequest(c, err) } if errors := h.validator.ValidateMessageThreadIndex(ctx, request.Sanitize()); len(errors) != 0 { - ctxLogger.Warn(stacktrace.NewError("validation errors [%s], while fetching message threads [%+#v]", spew.Sdump(errors), request)) + ctxLogger.Warn(stacktrace.NewErrorf("validation errors [%s], while fetching message threads [%+#v]", spew.Sdump(errors), request)) return h.responseUnprocessableEntity(c, errors, "validation errors while fetching message threads") } threads, err := h.service.GetThreads(ctx, request.ToGetParams(h.userIDFomContext(c))) if err != nil { - ctxLogger.Error(stacktrace.Propagate(err, "cannot get message threads with params [%+#v]", request)) + ctxLogger.Error(stacktrace.Propagatef(err, "cannot get message threads with params [%+#v]", request)) return h.responseInternalServerError(c) } @@ -113,13 +113,13 @@ func (h *MessageThreadHandler) Update(c fiber.Ctx) error { var request requests.MessageThreadUpdate if err := c.Bind().Body(&request); err != nil { - ctxLogger.Warn(stacktrace.Propagate(err, "cannot marshall params [%s] into %T", c.OriginalURL(), request)) + ctxLogger.Warn(stacktrace.Propagatef(err, "cannot marshall params [%s] into %T", c.OriginalURL(), request)) return h.responseBadRequest(c, err) } request.MessageThreadID = c.Params("messageThreadID") if errors := h.validator.ValidateUpdate(ctx, request); len(errors) != 0 { - ctxLogger.Warn(stacktrace.NewError("validation errors [%s], while updating message thread [%+#v]", spew.Sdump(errors), request)) + ctxLogger.Warn(stacktrace.NewErrorf("validation errors [%s], while updating message thread [%+#v]", spew.Sdump(errors), request)) return h.responseUnprocessableEntity(c, errors, "validation errors while updating message thread") } @@ -128,7 +128,7 @@ func (h *MessageThreadHandler) Update(c fiber.Ctx) error { return h.responseNotFound(c, fmt.Sprintf("cannot find message thread with ID [%s]", request.MessageThreadID)) } if err != nil { - ctxLogger.Error(stacktrace.Propagate(err, "cannot update message thread with params [%+#v]", request)) + ctxLogger.Error(stacktrace.Propagatef(err, "cannot update message thread with params [%+#v]", request)) return h.responseInternalServerError(c) } @@ -156,7 +156,7 @@ func (h *MessageThreadHandler) Delete(c fiber.Ctx) error { messageThreadID := c.Params("messageThreadID") if errors := h.validator.ValidateUUID(messageThreadID, "messageThreadID"); len(errors) != 0 { - ctxLogger.Warn(stacktrace.NewError("validation errors [%s], while deleting a thread thread with ID [%s]", spew.Sdump(errors), messageThreadID)) + ctxLogger.Warn(stacktrace.NewErrorf("validation errors [%s], while deleting a thread thread with ID [%s]", spew.Sdump(errors), messageThreadID)) return h.responseUnprocessableEntity(c, errors, "validation errors while deleting a thread thread") } @@ -166,12 +166,12 @@ func (h *MessageThreadHandler) Delete(c fiber.Ctx) error { } if err != nil { - ctxLogger.Error(h.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot find thread thread with id [%s]", messageThreadID))) + ctxLogger.Error(h.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot find thread thread with id [%s]", messageThreadID))) return h.responseInternalServerError(c) } if err = h.service.DeleteThread(ctx, c.OriginalURL(), thread); err != nil { - ctxLogger.Error(h.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot delete thread thread with ID [%s] for user with ID [%s]", messageThreadID, thread.UserID))) + ctxLogger.Error(h.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot delete thread thread with ID [%s] for user with ID [%s]", messageThreadID, thread.UserID))) return h.responseInternalServerError(c) } diff --git a/api/pkg/handlers/message_thread_handler_test.go b/api/pkg/handlers/message_thread_handler_test.go index 60b9d756e..fefb81895 100644 --- a/api/pkg/handlers/message_thread_handler_test.go +++ b/api/pkg/handlers/message_thread_handler_test.go @@ -34,7 +34,7 @@ func (stub *messageThreadHandlerRepositoryStub) UpdateActivity(context.Context, } func (stub *messageThreadHandlerRepositoryStub) UpdateStatus(context.Context, entities.UserID, uuid.UUID, repositories.MessageThreadStatusUpdate) (*entities.MessageThread, error) { - return nil, stacktrace.PropagateWithCode(gorm.ErrRecordNotFound, repositories.ErrCodeNotFound, "not found") + return nil, stacktrace.PropagateWithCodef(gorm.ErrRecordNotFound, repositories.ErrCodeNotFound, "not found") } func (stub *messageThreadHandlerRepositoryStub) LoadByOwnerContact(context.Context, entities.UserID, string, string) (*entities.MessageThread, error) { diff --git a/api/pkg/handlers/phone_api_key_handler.go b/api/pkg/handlers/phone_api_key_handler.go index 55b981cf4..09f893404 100644 --- a/api/pkg/handlers/phone_api_key_handler.go +++ b/api/pkg/handlers/phone_api_key_handler.go @@ -72,18 +72,18 @@ func (h *PhoneAPIKeyHandler) index(c fiber.Ctx) error { var request requests.PhoneAPIKeyIndex if err := c.Bind().Query(&request); err != nil { - ctxLogger.Warn(stacktrace.Propagate(err, "cannot marshall params [%s] into %T", c.OriginalURL(), request)) + ctxLogger.Warn(stacktrace.Propagatef(err, "cannot marshall params [%s] into %T", c.OriginalURL(), request)) return h.responseBadRequest(c, err) } if errors := h.validator.ValidateIndex(ctx, request.Sanitize()); len(errors) != 0 { - ctxLogger.Warn(stacktrace.NewError("validation errors [%s], while fetching phone API keys [%+#v]", spew.Sdump(errors), request)) + ctxLogger.Warn(stacktrace.NewErrorf("validation errors [%s], while fetching phone API keys [%+#v]", spew.Sdump(errors), request)) return h.responseUnprocessableEntity(c, errors, "validation errors while fetching phone API keys") } apiKeys, err := h.service.Index(ctx, h.userIDFomContext(c), request.ToIndexParams()) if err != nil { - ctxLogger.Error(stacktrace.Propagate(err, "cannot index phone API keys with params [%+#v]", request)) + ctxLogger.Error(stacktrace.Propagatef(err, "cannot index phone API keys with params [%+#v]", request)) return h.responseInternalServerError(c) } @@ -114,7 +114,7 @@ func (h *PhoneAPIKeyHandler) store(c fiber.Ctx) error { return h.service.CountByUser(ctx, userID) }) if err != nil { - ctxLogger.Error(stacktrace.Propagate(err, "cannot check entitlement for phone API keys for user [%s]", userID)) + ctxLogger.Error(stacktrace.Propagatef(err, "cannot check entitlement for phone API keys for user [%s]", userID)) return h.responseInternalServerError(c) } if !result.Allowed { @@ -123,18 +123,18 @@ func (h *PhoneAPIKeyHandler) store(c fiber.Ctx) error { var request requests.PhoneAPIKeyStoreRequest if err := c.Bind().Body(&request); err != nil { - ctxLogger.Warn(stacktrace.Propagate(err, "cannot marshall params [%s] into %T", c.OriginalURL(), request)) + ctxLogger.Warn(stacktrace.Propagatef(err, "cannot marshall params [%s] into %T", c.OriginalURL(), request)) return h.responseBadRequest(c, err) } if errors := h.validator.ValidateStore(ctx, request.Sanitize()); len(errors) != 0 { - ctxLogger.Warn(stacktrace.NewError("validation errors [%s], while updating phones [%+#v]", spew.Sdump(errors), request)) + ctxLogger.Warn(stacktrace.NewErrorf("validation errors [%s], while updating phones [%+#v]", spew.Sdump(errors), request)) return h.responseUnprocessableEntity(c, errors, "validation errors while updating phones") } phoneAPIKey, err := h.service.Create(ctx, h.userFromContext(c), request.Name) if err != nil { - ctxLogger.Error(stacktrace.Propagate(err, "cannot update phones with params [%+#v]", request)) + ctxLogger.Error(stacktrace.Propagatef(err, "cannot update phones with params [%+#v]", request)) return h.responseInternalServerError(c) } @@ -161,7 +161,7 @@ func (h *PhoneAPIKeyHandler) delete(c fiber.Ctx) error { phoneAPIKeyID := c.Params("phoneAPIKeyID") if errors := h.validator.ValidateUUID(phoneAPIKeyID, "phoneAPIKeyID"); len(errors) != 0 { - ctxLogger.Warn(stacktrace.NewError("validation errors [%s], while deleting a phone API key with ID [%s]", spew.Sdump(errors), phoneAPIKeyID)) + ctxLogger.Warn(stacktrace.NewErrorf("validation errors [%s], while deleting a phone API key with ID [%s]", spew.Sdump(errors), phoneAPIKeyID)) return h.responseUnprocessableEntity(c, errors, "validation errors while storing event") } @@ -171,7 +171,7 @@ func (h *PhoneAPIKeyHandler) delete(c fiber.Ctx) error { } if err != nil { - ctxLogger.Error(h.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot delete phone API key with ID [%s] for user with ID [%s]", phoneAPIKeyID, h.userIDFomContext(c)))) + ctxLogger.Error(h.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot delete phone API key with ID [%s] for user with ID [%s]", phoneAPIKeyID, h.userIDFomContext(c)))) return h.responseInternalServerError(c) } @@ -200,7 +200,7 @@ func (h *PhoneAPIKeyHandler) deletePhone(c fiber.Ctx) error { phoneAPIKeyID := c.Params("phoneAPIKeyID") phoneID := c.Params("phoneID") if errors := h.mergeErrors(h.validator.ValidateUUID(phoneAPIKeyID, "phoneAPIKeyID"), h.validator.ValidateUUID(phoneID, "phoneID")); len(errors) != 0 { - ctxLogger.Warn(stacktrace.NewError("validation errors [%s], while deleting a phone API key with ID [%s]", spew.Sdump(errors), phoneAPIKeyID)) + ctxLogger.Warn(stacktrace.NewErrorf("validation errors [%s], while deleting a phone API key with ID [%s]", spew.Sdump(errors), phoneAPIKeyID)) return h.responseUnprocessableEntity(c, errors, "validation errors while storing event") } @@ -210,7 +210,7 @@ func (h *PhoneAPIKeyHandler) deletePhone(c fiber.Ctx) error { } if err != nil { - ctxLogger.Error(h.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot remove phone with ID [%s] from phone API key with ID [%s] for user with ID [%s]", phoneID, phoneAPIKeyID, h.userIDFomContext(c)))) + ctxLogger.Error(h.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot remove phone with ID [%s] from phone API key with ID [%s] for user with ID [%s]", phoneID, phoneAPIKeyID, h.userIDFomContext(c)))) return h.responseInternalServerError(c) } diff --git a/api/pkg/handlers/phone_handler.go b/api/pkg/handlers/phone_handler.go index 678b89062..55380ed91 100644 --- a/api/pkg/handlers/phone_handler.go +++ b/api/pkg/handlers/phone_handler.go @@ -74,18 +74,18 @@ func (h *PhoneHandler) Index(c fiber.Ctx) error { var request requests.PhoneIndex if err := c.Bind().Query(&request); err != nil { - ctxLogger.Warn(stacktrace.Propagate(err, "cannot marshall params [%s] into %T", c.OriginalURL(), request)) + ctxLogger.Warn(stacktrace.Propagatef(err, "cannot marshall params [%s] into %T", c.OriginalURL(), request)) return h.responseBadRequest(c, err) } if errors := h.validator.ValidateIndex(ctx, request.Sanitize()); len(errors) != 0 { - ctxLogger.Warn(stacktrace.NewError("validation errors [%s], while fetching phones [%+#v]", spew.Sdump(errors), request)) + ctxLogger.Warn(stacktrace.NewErrorf("validation errors [%s], while fetching phones [%+#v]", spew.Sdump(errors), request)) return h.responseUnprocessableEntity(c, errors, "validation errors while fetching phones") } phones, err := h.service.Index(ctx, h.userFromContext(c), request.ToIndexParams()) if err != nil { - ctxLogger.Error(stacktrace.Propagate(err, "cannot index phones with params [%+#v]", request)) + ctxLogger.Error(stacktrace.Propagatef(err, "cannot index phones with params [%+#v]", request)) return h.responseInternalServerError(c) } @@ -114,18 +114,18 @@ func (h *PhoneHandler) Upsert(c fiber.Ctx) error { var request requests.PhoneUpsert if err := c.Bind().Body(&request); err != nil { - ctxLogger.Warn(stacktrace.Propagate(err, "cannot marshall params [%s] into %T", c.OriginalURL(), request)) + ctxLogger.Warn(stacktrace.Propagatef(err, "cannot marshall params [%s] into %T", c.OriginalURL(), request)) return h.responseBadRequest(c, err) } if errors := h.validator.ValidateUpsert(ctx, h.userIDFomContext(c), request.Sanitize()); len(errors) != 0 { - ctxLogger.Warn(stacktrace.NewError("validation errors [%s], while updating phones [%+#v]", spew.Sdump(errors), request)) + ctxLogger.Warn(stacktrace.NewErrorf("validation errors [%s], while updating phones [%+#v]", spew.Sdump(errors), request)) return h.responseUnprocessableEntity(c, errors, "validation errors while updating phones") } phone, err := h.service.Upsert(ctx, request.ToUpsertParams(h.userFromContext(c), c.OriginalURL(), c.Body())) if err != nil { - ctxLogger.Error(stacktrace.Propagate(err, "cannot update phones with params [%+#v]", request)) + ctxLogger.Error(stacktrace.Propagatef(err, "cannot update phones with params [%+#v]", request)) return h.responseInternalServerError(c) } @@ -154,7 +154,7 @@ func (h *PhoneHandler) Delete(c fiber.Ctx) error { request := requests.PhoneDelete{PhoneID: c.Params("phoneID")} if errors := h.validator.ValidateDelete(ctx, request); len(errors) != 0 { - ctxLogger.Warn(stacktrace.NewError("validation errors [%s], while deleting phone [%+#v]", spew.Sdump(errors), request)) + ctxLogger.Warn(stacktrace.NewErrorf("validation errors [%s], while deleting phone [%+#v]", spew.Sdump(errors), request)) return h.responseUnprocessableEntity(c, errors, "validation errors while deleting phone") } @@ -163,7 +163,7 @@ func (h *PhoneHandler) Delete(c fiber.Ctx) error { return h.responseNotFound(c, fmt.Sprintf("cannot find phone with ID [%s]", request.PhoneID)) } if err != nil { - ctxLogger.Error(stacktrace.Propagate(err, "cannot delete phones with params [%+#v]", request)) + ctxLogger.Error(stacktrace.Propagatef(err, "cannot delete phones with params [%+#v]", request)) return h.responseInternalServerError(c) } @@ -192,18 +192,18 @@ func (h *PhoneHandler) UpsertFCMToken(c fiber.Ctx) error { var request requests.PhoneFCMToken if err := c.Bind().Body(&request); err != nil { - ctxLogger.Warn(stacktrace.Propagate(err, "cannot marshall params [%s] into %T", c.OriginalURL(), request)) + ctxLogger.Warn(stacktrace.Propagatef(err, "cannot marshall params [%s] into %T", c.OriginalURL(), request)) return h.responseBadRequest(c, err) } if errors := h.validator.ValidateFCMToken(ctx, request.Sanitize()); len(errors) != 0 { - ctxLogger.Warn(stacktrace.NewError("validation errors [%s], while updating phones [%+#v]", spew.Sdump(errors), request)) + ctxLogger.Warn(stacktrace.NewErrorf("validation errors [%s], while updating phones [%+#v]", spew.Sdump(errors), request)) return h.responseUnprocessableEntity(c, errors, "validation errors while updating phones") } phone, err := h.service.UpsertFCMToken(ctx, request.ToPhoneFCMTokenParams(h.userFromContext(c), c.OriginalURL())) if err != nil { - ctxLogger.Error(stacktrace.Propagate(err, "cannot delete phones with params [%+#v]", request)) + ctxLogger.Error(stacktrace.Propagatef(err, "cannot delete phones with params [%+#v]", request)) return h.responseInternalServerError(c) } diff --git a/api/pkg/handlers/user_handler.go b/api/pkg/handlers/user_handler.go index 8f73ba00c..b5f224fd2 100644 --- a/api/pkg/handlers/user_handler.go +++ b/api/pkg/handlers/user_handler.go @@ -71,7 +71,7 @@ func (h *UserHandler) Show(c fiber.Ctx) error { authUser := h.userFromContext(c) user, err := h.service.Get(ctx, c.OriginalURL(), authUser) if err != nil { - ctxLogger.Error(stacktrace.Propagate(err, "cannot get user with ID [%s]", authUser.ID)) + ctxLogger.Error(stacktrace.Propagatef(err, "cannot get user with ID [%s]", authUser.ID)) return h.responseInternalServerError(c) } @@ -100,18 +100,18 @@ func (h *UserHandler) Update(c fiber.Ctx) error { var request requests.UserUpdate if err := c.Bind().Body(&request); err != nil { - ctxLogger.Warn(stacktrace.Propagate(err, "cannot marshall params [%s] into %T", c.OriginalURL(), request)) + ctxLogger.Warn(stacktrace.Propagatef(err, "cannot marshall params [%s] into %T", c.OriginalURL(), request)) return h.responseBadRequest(c, err) } if errors := h.validator.ValidateUpdate(ctx, request.Sanitize()); len(errors) != 0 { - ctxLogger.Warn(stacktrace.NewError("validation errors [%s], while updating user [%+#v]", spew.Sdump(errors), request)) + ctxLogger.Warn(stacktrace.NewErrorf("validation errors [%s], while updating user [%+#v]", spew.Sdump(errors), request)) return h.responseUnprocessableEntity(c, errors, "validation errors while updating user") } user, err := h.service.Update(ctx, c.OriginalURL(), h.userFromContext(c), request.ToUpdateParams()) if err != nil { - ctxLogger.Error(stacktrace.Propagate(err, "cannot update user with params [%+#v]", request)) + ctxLogger.Error(stacktrace.Propagatef(err, "cannot update user with params [%+#v]", request)) return h.responseInternalServerError(c) } @@ -134,7 +134,7 @@ func (h *UserHandler) Delete(c fiber.Ctx) error { defer span.End() if err := h.service.Delete(ctx, c.OriginalURL(), h.userIDFomContext(c)); err != nil { - ctxLogger.Error(stacktrace.Propagate(err, "cannot delete user user with ID [%s]", h.userIDFomContext(c))) + ctxLogger.Error(stacktrace.Propagatef(err, "cannot delete user user with ID [%s]", h.userIDFomContext(c))) return h.responseInternalServerError(c) } @@ -162,13 +162,13 @@ func (h *UserHandler) UpdateNotifications(c fiber.Ctx) error { var request requests.UserNotificationUpdate if err := c.Bind().Body(&request); err != nil { - ctxLogger.Warn(stacktrace.Propagate(err, "cannot marshall params [%s] into %T", c.OriginalURL(), request)) + ctxLogger.Warn(stacktrace.Propagatef(err, "cannot marshall params [%s] into %T", c.OriginalURL(), request)) return h.responseBadRequest(c, err) } user, err := h.service.UpdateNotificationSettings(ctx, h.userIDFomContext(c), request.ToUserNotificationUpdateParams()) if err != nil { - ctxLogger.Error(h.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot update notification for [%T] with ID [%s]", user, h.userIDFomContext(c)))) + ctxLogger.Error(h.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot update notification for [%T] with ID [%s]", user, h.userIDFomContext(c)))) return h.responseInternalServerError(c) } @@ -196,7 +196,7 @@ func (h *UserHandler) subscriptionUpdateURL(c fiber.Ctx) error { url, err := h.service.GetSubscriptionUpdateURL(ctx, authUser.ID) if err != nil { - ctxLogger.Error(stacktrace.Propagate(err, "cannot get user with ID [%s]", authUser.ID)) + ctxLogger.Error(stacktrace.Propagatef(err, "cannot get user with ID [%s]", authUser.ID)) return h.responseInternalServerError(c) } @@ -224,7 +224,7 @@ func (h *UserHandler) cancelSubscription(c fiber.Ctx) error { err := h.service.InitiateSubscriptionCancel(ctx, authUser.ID) if err != nil { - ctxLogger.Error(stacktrace.Propagate(err, "cannot get user with ID [%s]", authUser.ID)) + ctxLogger.Error(stacktrace.Propagatef(err, "cannot get user with ID [%s]", authUser.ID)) return h.responseInternalServerError(c) } @@ -257,7 +257,7 @@ func (h *UserHandler) DeleteAPIKey(c fiber.Ctx) error { user, err := h.service.RotateAPIKey(ctx, c.OriginalURL(), h.userIDFomContext(c)) if err != nil { - ctxLogger.Error(h.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot rotate the api key for [%T] with ID [%s]", user, h.userIDFomContext(c)))) + ctxLogger.Error(h.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot rotate the api key for [%T] with ID [%s]", user, h.userIDFomContext(c)))) return h.responseInternalServerError(c) } @@ -283,7 +283,7 @@ func (h *UserHandler) subscriptionPayments(c fiber.Ctx) error { invoices, err := h.service.GetSubscriptionPayments(ctx, h.userIDFomContext(c)) if err != nil { - ctxLogger.Error(stacktrace.Propagate(err, "cannot get current subscription invoices for user [%s]", h.userFromContext(c))) + ctxLogger.Error(stacktrace.Propagatef(err, "cannot get current subscription invoices for user [%s]", h.userFromContext(c))) return h.responseInternalServerError(c) } @@ -311,19 +311,19 @@ func (h *UserHandler) subscriptionInvoice(c fiber.Ctx) error { var request requests.UserPaymentInvoice if err := c.Bind().Body(&request); err != nil { - ctxLogger.Warn(stacktrace.Propagate(err, "cannot marshall params [%s] into %T", c.Body(), request)) + ctxLogger.Warn(stacktrace.Propagatef(err, "cannot marshall params [%s] into %T", c.Body(), request)) return h.responseBadRequest(c, err) } request.SubscriptionInvoiceID = c.Params("subscriptionInvoiceID") if errors := h.validator.ValidatePaymentInvoice(ctx, h.userIDFomContext(c), request.Sanitize()); len(errors) != 0 { - ctxLogger.Warn(stacktrace.NewError("validation errors [%s], while validating subscription payment invoice request [%s]", spew.Sdump(errors), c.Body())) + ctxLogger.Warn(stacktrace.NewErrorf("validation errors [%s], while validating subscription payment invoice request [%s]", spew.Sdump(errors), c.Body())) return h.responseUnprocessableEntity(c, errors, "validation errors while generating payment invoice") } reader, err := h.service.GenerateReceipt(ctx, request.UserInvoiceGenerateParams(h.userIDFomContext(c))) if err != nil { - ctxLogger.Error(stacktrace.Propagate(err, "cannot generate receipt for invoice ID [%s] and user [%s]", request.SubscriptionInvoiceID, h.userFromContext(c))) + ctxLogger.Error(stacktrace.Propagatef(err, "cannot generate receipt for invoice ID [%s] and user [%s]", request.SubscriptionInvoiceID, h.userFromContext(c))) return h.responseInternalServerError(c) } @@ -332,7 +332,7 @@ func (h *UserHandler) subscriptionInvoice(c fiber.Ctx) error { data, err := io.ReadAll(reader) if err != nil { - ctxLogger.Error(stacktrace.Propagate(err, "cannot read invoice data with ID [%s] for user with ID [%s]", request.SubscriptionInvoiceID, h.userIDFomContext(c))) + ctxLogger.Error(stacktrace.Propagatef(err, "cannot read invoice data with ID [%s] for user with ID [%s]", request.SubscriptionInvoiceID, h.userIDFomContext(c))) return h.responseInternalServerError(c) } diff --git a/api/pkg/handlers/webhook_handler.go b/api/pkg/handlers/webhook_handler.go index 6eb389e3c..6866da6bc 100644 --- a/api/pkg/handlers/webhook_handler.go +++ b/api/pkg/handlers/webhook_handler.go @@ -70,18 +70,18 @@ func (h *WebhookHandler) Index(c fiber.Ctx) error { var request requests.WebhookIndex if err := c.Bind().Query(&request); err != nil { - ctxLogger.Warn(stacktrace.Propagate(err, "cannot marshall URL [%s] into %T", c.OriginalURL(), request)) + ctxLogger.Warn(stacktrace.Propagatef(err, "cannot marshall URL [%s] into %T", c.OriginalURL(), request)) return h.responseBadRequest(c, err) } if errors := h.validator.ValidateIndex(ctx, request.Sanitize()); len(errors) != 0 { - ctxLogger.Warn(stacktrace.NewError("validation errors [%s], while fetching webhooks [%+#v]", spew.Sdump(errors), request)) + ctxLogger.Warn(stacktrace.NewErrorf("validation errors [%s], while fetching webhooks [%+#v]", spew.Sdump(errors), request)) return h.responseUnprocessableEntity(c, errors, "validation errors while fetching webhooks") } webhooks, err := h.service.Index(ctx, h.userIDFomContext(c), request.ToIndexParams()) if err != nil { - ctxLogger.Error(stacktrace.Propagate(err, "cannot get webhooks with params [%+#v]", request)) + ctxLogger.Error(stacktrace.Propagatef(err, "cannot get webhooks with params [%+#v]", request)) return h.responseInternalServerError(c) } @@ -108,13 +108,13 @@ func (h *WebhookHandler) Delete(c fiber.Ctx) error { webhookID := c.Params("webhookID") if errors := h.validator.ValidateUUID(webhookID, "webhookID"); len(errors) != 0 { - ctxLogger.Warn(stacktrace.NewError("validation errors [%s], while deleting webhook with ID [%s]", spew.Sdump(errors), webhookID)) + ctxLogger.Warn(stacktrace.NewErrorf("validation errors [%s], while deleting webhook with ID [%s]", spew.Sdump(errors), webhookID)) return h.responseUnprocessableEntity(c, errors, "validation errors while deleting webhook") } err := h.service.Delete(ctx, h.userIDFomContext(c), uuid.MustParse(webhookID)) if err != nil { - ctxLogger.Error(stacktrace.Propagate(err, "cannot delete webhook with ID [%+#v]", webhookID)) + ctxLogger.Error(stacktrace.Propagatef(err, "cannot delete webhook with ID [%+#v]", webhookID)) return h.responseInternalServerError(c) } @@ -143,29 +143,29 @@ func (h *WebhookHandler) Store(c fiber.Ctx) error { var request requests.WebhookStore if err := c.Bind().Body(&request); err != nil { - ctxLogger.Warn(stacktrace.Propagate(err, "cannot marshall body [%s] into [%T]", c.Body(), request)) + ctxLogger.Warn(stacktrace.Propagatef(err, "cannot marshall body [%s] into [%T]", c.Body(), request)) return h.responseBadRequest(c, err) } if errors := h.validator.ValidateStore(ctx, h.userIDFomContext(c), request.Sanitize()); len(errors) != 0 { - ctxLogger.Warn(stacktrace.NewError("validation errors [%s], while storing webhook [%+#v]", spew.Sdump(errors), request)) + ctxLogger.Warn(stacktrace.NewErrorf("validation errors [%s], while storing webhook [%+#v]", spew.Sdump(errors), request)) return h.responseUnprocessableEntity(c, errors, "validation errors while storing webhook") } webhooks, err := h.service.Index(ctx, h.userIDFomContext(c), repositories.IndexParams{Skip: 0, Limit: 10}) if err != nil { - ctxLogger.Error(stacktrace.Propagate(err, "cannot index webhooks for user [%s]", h.userIDFomContext(c))) + ctxLogger.Error(stacktrace.Propagatef(err, "cannot index webhooks for user [%s]", h.userIDFomContext(c))) return h.responseInternalServerError(c) } if len(webhooks) == 10 { - ctxLogger.Warn(stacktrace.NewError("user with ID [%s] wants to create more than 10 webhooks", h.userIDFomContext(c))) + ctxLogger.Warn(stacktrace.NewErrorf("user with ID [%s] wants to create more than 10 webhooks", h.userIDFomContext(c))) return h.responsePaymentRequired(c, "You can't create more than 10 webhooks contact us to upgrade to our enterprise plan.") } webhook, err := h.service.Store(ctx, request.ToStoreParams(h.userFromContext(c))) if err != nil { - ctxLogger.Error(stacktrace.Propagate(err, "cannot store webhoook with params [%+#v]", request)) + ctxLogger.Error(stacktrace.Propagatef(err, "cannot store webhoook with params [%+#v]", request)) return h.responseInternalServerError(c) } @@ -193,19 +193,19 @@ func (h *WebhookHandler) Update(c fiber.Ctx) error { var request requests.WebhookUpdate if err := c.Bind().Body(&request); err != nil { - ctxLogger.Warn(stacktrace.Propagate(err, "cannot marshall params [%s] into [%T]", c.Body(), request)) + ctxLogger.Warn(stacktrace.Propagatef(err, "cannot marshall params [%s] into [%T]", c.Body(), request)) return h.responseBadRequest(c, err) } request.WebhookID = c.Params("webhookID") if errors := h.validator.ValidateUpdate(ctx, h.userIDFomContext(c), request.Sanitize()); len(errors) != 0 { - ctxLogger.Warn(stacktrace.NewError("validation errors [%s], while updating user [%+#v]", spew.Sdump(errors), request)) + ctxLogger.Warn(stacktrace.NewErrorf("validation errors [%s], while updating user [%+#v]", spew.Sdump(errors), request)) return h.responseUnprocessableEntity(c, errors, "validation errors while updating webhook") } user, err := h.service.Update(ctx, request.ToUpdateParams(h.userFromContext(c))) if err != nil { - ctxLogger.Error(stacktrace.Propagate(err, "cannot update user with params [%+#v]", request)) + ctxLogger.Error(stacktrace.Propagatef(err, "cannot update user with params [%+#v]", request)) return h.responseInternalServerError(c) } diff --git a/api/pkg/listeners/billing_listener.go b/api/pkg/listeners/billing_listener.go index 2645e27c3..08badc22d 100644 --- a/api/pkg/listeners/billing_listener.go +++ b/api/pkg/listeners/billing_listener.go @@ -46,11 +46,11 @@ func (listener *BillingListener) OnMessageAPISent(ctx context.Context, event clo var payload events.MessageAPISentPayload if err := event.DataAs(&payload); err != nil { - return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot decode [%s] into [%T]", event.Data(), payload)) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot decode [%s] into [%T]", event.Data(), payload)) } if err := listener.service.RegisterSentMessage(ctx, payload.MessageID, payload.RequestReceivedAt, payload.UserID); err != nil { - return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot register sent message for event [%s] for event with ID [%s]", spew.Sdump(payload), event.ID())) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot register sent message for event [%s] for event with ID [%s]", spew.Sdump(payload), event.ID())) } return nil @@ -63,11 +63,11 @@ func (listener *BillingListener) OnMessagePhoneReceived(ctx context.Context, eve var payload events.MessagePhoneReceivedPayload if err := event.DataAs(&payload); err != nil { - return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot decode [%s] into [%T]", event.Data(), payload)) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot decode [%s] into [%T]", event.Data(), payload)) } if err := listener.service.RegisterReceivedMessage(ctx, payload.MessageID, payload.Timestamp, payload.UserID); err != nil { - return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot register received message for event [%s] for event with ID [%s]", spew.Sdump(payload), event.ID())) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot register received message for event [%s] for event with ID [%s]", spew.Sdump(payload), event.ID())) } return nil @@ -79,11 +79,11 @@ func (listener *BillingListener) onUserAccountDeleted(ctx context.Context, event var payload events.UserAccountDeletedPayload if err := event.DataAs(&payload); err != nil { - return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot decode [%s] into [%T]", event.Data(), payload)) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot decode [%s] into [%T]", event.Data(), payload)) } if err := listener.service.DeleteAllForUser(ctx, payload.UserID); err != nil { - return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot delete [entities.BillingUsage] for user [%s] on [%s] event with ID [%s]", payload.UserID, event.Type(), event.ID())) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot delete [entities.BillingUsage] for user [%s] on [%s] event with ID [%s]", payload.UserID, event.Type(), event.ID())) } return nil diff --git a/api/pkg/listeners/discord_listener.go b/api/pkg/listeners/discord_listener.go index aebed46b7..e0514177e 100644 --- a/api/pkg/listeners/discord_listener.go +++ b/api/pkg/listeners/discord_listener.go @@ -43,11 +43,11 @@ func (listener *DiscordListener) OnMessagePhoneReceived(ctx context.Context, eve var payload events.MessagePhoneReceivedPayload if err := event.DataAs(&payload); err != nil { - return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot decode [%s] into [%T]", event.Data(), payload)) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot decode [%s] into [%T]", event.Data(), payload)) } if err := listener.service.HandleMessageReceived(ctx, payload.UserID, event); err != nil { - return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot process [%s] event with ID [%s]", event.Type(), event.ID())) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot process [%s] event with ID [%s]", event.Type(), event.ID())) } return nil @@ -59,11 +59,11 @@ func (listener *DiscordListener) onUserAccountDeleted(ctx context.Context, event var payload events.UserAccountDeletedPayload if err := event.DataAs(&payload); err != nil { - return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot decode [%s] into [%T]", event.Data(), payload)) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot decode [%s] into [%T]", event.Data(), payload)) } if err := listener.service.DeleteAllForUser(ctx, payload.UserID); err != nil { - return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot delete [entities.Discord] for user [%s] on [%s] event with ID [%s]", payload.UserID, event.Type(), event.ID())) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot delete [entities.Discord] for user [%s] on [%s] event with ID [%s]", payload.UserID, event.Type(), event.ID())) } return nil diff --git a/api/pkg/listeners/email_notification_listener.go b/api/pkg/listeners/email_notification_listener.go index fc6065398..bc78f993e 100644 --- a/api/pkg/listeners/email_notification_listener.go +++ b/api/pkg/listeners/email_notification_listener.go @@ -45,11 +45,11 @@ func (listener *EmailNotificationListener) OnMessageSendExpired(ctx context.Cont payload := new(events.MessageSendExpiredPayload) if err := event.DataAs(&payload); err != nil { - return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot decode [%s] into [%T]", event.Data(), payload)) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot decode [%s] into [%T]", event.Data(), payload)) } if err := listener.service.NotifyMessageExpired(ctx, payload); err != nil { - return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot process [%s] event with ID [%s]", event.Type(), event.ID())) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot process [%s] event with ID [%s]", event.Type(), event.ID())) } return nil @@ -62,11 +62,11 @@ func (listener *EmailNotificationListener) OnMessageSendFailed(ctx context.Conte payload := new(events.MessageSendFailedPayload) if err := event.DataAs(&payload); err != nil { - return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot decode [%s] into [%T]", event.Data(), payload)) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot decode [%s] into [%T]", event.Data(), payload)) } if err := listener.service.NotifyMessageFailed(ctx, payload); err != nil { - return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot process [%s] event with ID [%s]", event.Type(), event.ID())) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot process [%s] event with ID [%s]", event.Type(), event.ID())) } return nil @@ -79,11 +79,11 @@ func (listener *EmailNotificationListener) OnWebhookSendFailed(ctx context.Conte payload := new(events.WebhookSendFailedPayload) if err := event.DataAs(&payload); err != nil { - return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot decode [%s] into [%T]", event.Data(), payload)) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot decode [%s] into [%T]", event.Data(), payload)) } if err := listener.service.NotifyWebhookSendFailed(ctx, payload); err != nil { - return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot process [%s] event with ID [%s]", event.Type(), event.ID())) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot process [%s] event with ID [%s]", event.Type(), event.ID())) } return nil @@ -96,11 +96,11 @@ func (listener *EmailNotificationListener) OnDiscordSendFailed(ctx context.Conte payload := new(events.DiscordSendFailedPayload) if err := event.DataAs(&payload); err != nil { - return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot decode [%s] into [%T]", event.Data(), payload)) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot decode [%s] into [%T]", event.Data(), payload)) } if err := listener.service.NotifyDiscordSendFailed(ctx, payload); err != nil { - return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot process [%s] event with ID [%s]", event.Type(), event.ID())) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot process [%s] event with ID [%s]", event.Type(), event.ID())) } return nil diff --git a/api/pkg/listeners/heartbeat_listener.go b/api/pkg/listeners/heartbeat_listener.go index eef4933f5..4e0cdc0d7 100644 --- a/api/pkg/listeners/heartbeat_listener.go +++ b/api/pkg/listeners/heartbeat_listener.go @@ -48,7 +48,7 @@ func (listener *HeartbeatListener) onPhoneUpdated(ctx context.Context, event clo var payload events.PhoneUpdatedPayload if err := event.DataAs(&payload); err != nil { - return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot decode [%s] into [%T]", event.Data(), payload)) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot decode [%s] into [%T]", event.Data(), payload)) } storeParams := &services.HeartbeatMonitorStoreParams{ @@ -59,7 +59,7 @@ func (listener *HeartbeatListener) onPhoneUpdated(ctx context.Context, event clo } if _, err := listener.service.StoreMonitor(ctx, storeParams); err != nil { - return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot store heartbeat monitor with params [%s] for event with ID [%s]", spew.Sdump(storeParams), event.ID())) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot store heartbeat monitor with params [%s] for event with ID [%s]", spew.Sdump(storeParams), event.ID())) } return nil @@ -72,11 +72,11 @@ func (listener *HeartbeatListener) onPhoneDeleted(ctx context.Context, event clo var payload events.PhoneDeletedPayload if err := event.DataAs(&payload); err != nil { - return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot decode [%s] into [%T]", event.Data(), payload)) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot decode [%s] into [%T]", event.Data(), payload)) } if err := listener.service.DeleteMonitor(ctx, payload.UserID, payload.Owner); err != nil { - return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot delete heartbeat monitor with userID [%s] and owner [%s] for event with ID [%s]", payload.UserID, payload.Owner, event.ID())) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot delete heartbeat monitor with userID [%s] and owner [%s] for event with ID [%s]", payload.UserID, payload.Owner, event.ID())) } return nil @@ -89,7 +89,7 @@ func (listener *HeartbeatListener) onPhoneHeartbeatCheck(ctx context.Context, ev var payload events.PhoneHeartbeatCheckPayload if err := event.DataAs(&payload); err != nil { - return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot decode [%s] into [%T]", event.Data(), payload)) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot decode [%s] into [%T]", event.Data(), payload)) } monitorParams := &services.HeartbeatMonitorParams{ @@ -101,7 +101,7 @@ func (listener *HeartbeatListener) onPhoneHeartbeatCheck(ctx context.Context, ev } if err := listener.service.Monitor(ctx, monitorParams); err != nil { - return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot monitor heartbeats for userID [%s] and owner [%s] for event with ID [%s]", payload.UserID, payload.Owner, event.ID())) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot monitor heartbeats for userID [%s] and owner [%s] for event with ID [%s]", payload.UserID, payload.Owner, event.ID())) } return nil @@ -114,11 +114,11 @@ func (listener *HeartbeatListener) onPhoneHeartbeatOffline(ctx context.Context, var payload events.PhoneHeartbeatOfflinePayload if err := event.DataAs(&payload); err != nil { - return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot decode [%s] into [%T]", event.Data(), payload)) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot decode [%s] into [%T]", event.Data(), payload)) } if err := listener.service.UpdatePhoneOnline(ctx, payload.UserID, payload.MonitorID, false); err != nil { - return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot delete heartbeat monitor with userID [%s] and owner [%s] for event with ID [%s]", payload.UserID, payload.Owner, event.ID())) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot delete heartbeat monitor with userID [%s] and owner [%s] for event with ID [%s]", payload.UserID, payload.Owner, event.ID())) } return nil @@ -130,11 +130,11 @@ func (listener *HeartbeatListener) onUserAccountDeleted(ctx context.Context, eve var payload events.UserAccountDeletedPayload if err := event.DataAs(&payload); err != nil { - return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot decode [%s] into [%T]", event.Data(), payload)) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot decode [%s] into [%T]", event.Data(), payload)) } if err := listener.service.DeleteAllForUser(ctx, payload.UserID); err != nil { - return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot delete [entities.Heartbeat] for user [%s] on [%s] event with ID [%s]", payload.UserID, event.Type(), event.ID())) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot delete [entities.Heartbeat] for user [%s] on [%s] event with ID [%s]", payload.UserID, event.Type(), event.ID())) } return nil diff --git a/api/pkg/listeners/integration_3cx_listener.go b/api/pkg/listeners/integration_3cx_listener.go index 529916f86..314d6a291 100644 --- a/api/pkg/listeners/integration_3cx_listener.go +++ b/api/pkg/listeners/integration_3cx_listener.go @@ -46,11 +46,11 @@ func (listener *Integration3CXListener) OnMessagePhoneReceived(ctx context.Conte var payload events.MessagePhoneReceivedPayload if err := event.DataAs(&payload); err != nil { - return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot decode [%s] into [%T]", event.Data(), payload)) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot decode [%s] into [%T]", event.Data(), payload)) } if err := listener.service.Send(ctx, payload.UserID, event); err != nil { - return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot process [%s] event with ID [%s]", event.Type(), event.ID())) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot process [%s] event with ID [%s]", event.Type(), event.ID())) } return nil @@ -63,11 +63,11 @@ func (listener *Integration3CXListener) OnMessageSendFailed(ctx context.Context, var payload events.MessageSendFailedPayload if err := event.DataAs(&payload); err != nil { - return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot decode [%s] into [%T]", event.Data(), payload)) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot decode [%s] into [%T]", event.Data(), payload)) } if err := listener.service.Send(ctx, payload.UserID, event); err != nil { - return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot process [%s] event with ID [%s]", event.Type(), event.ID())) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot process [%s] event with ID [%s]", event.Type(), event.ID())) } return nil @@ -80,11 +80,11 @@ func (listener *Integration3CXListener) OnMessagePhoneSent(ctx context.Context, var payload events.MessagePhoneSentPayload if err := event.DataAs(&payload); err != nil { - return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot decode [%s] into [%T]", event.Data(), payload)) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot decode [%s] into [%T]", event.Data(), payload)) } if err := listener.service.Send(ctx, payload.UserID, event); err != nil { - return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot process [%s] event with ID [%s]", event.Type(), event.ID())) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot process [%s] event with ID [%s]", event.Type(), event.ID())) } return nil @@ -97,11 +97,11 @@ func (listener *Integration3CXListener) OnMessagePhoneDelivered(ctx context.Cont var payload events.MessagePhoneDeliveredPayload if err := event.DataAs(&payload); err != nil { - return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot decode [%s] into [%T]", event.Data(), payload)) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot decode [%s] into [%T]", event.Data(), payload)) } if err := listener.service.Send(ctx, payload.UserID, event); err != nil { - return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot process [%s] event with ID [%s]", event.Type(), event.ID())) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot process [%s] event with ID [%s]", event.Type(), event.ID())) } return nil @@ -113,11 +113,11 @@ func (listener *Integration3CXListener) onUserAccountDeleted(ctx context.Context var payload events.UserAccountDeletedPayload if err := event.DataAs(&payload); err != nil { - return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot decode [%s] into [%T]", event.Data(), payload)) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot decode [%s] into [%T]", event.Data(), payload)) } if err := listener.service.DeleteAllForUser(ctx, payload.UserID); err != nil { - return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot delete [entities.Integration3CX] for user [%s] on [%s] event with ID [%s]", payload.UserID, event.Type(), event.ID())) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot delete [entities.Integration3CX] for user [%s] on [%s] event with ID [%s]", payload.UserID, event.Type(), event.ID())) } return nil diff --git a/api/pkg/listeners/marketing_listener.go b/api/pkg/listeners/marketing_listener.go index 4bcf15d11..fd2272a07 100644 --- a/api/pkg/listeners/marketing_listener.go +++ b/api/pkg/listeners/marketing_listener.go @@ -42,11 +42,11 @@ func (listener *MarketingListener) onUserAccountCreated(ctx context.Context, eve var payload events.UserAccountCreatedPayload if err := event.DataAs(&payload); err != nil { - return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot decode [%s] into [%T]", event.Data(), payload)) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot decode [%s] into [%T]", event.Data(), payload)) } if err := listener.service.CreateContact(ctx, payload.UserID); err != nil { - return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot create [contact] for user [%s] on [%s] event with ID [%s]", payload.UserID, event.Type(), event.ID())) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot create [contact] for user [%s] on [%s] event with ID [%s]", payload.UserID, event.Type(), event.ID())) } return nil @@ -58,11 +58,11 @@ func (listener *MarketingListener) onUserAccountDeleted(ctx context.Context, eve var payload events.UserAccountDeletedPayload if err := event.DataAs(&payload); err != nil { - return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot decode [%s] into [%T]", event.Data(), payload)) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot decode [%s] into [%T]", event.Data(), payload)) } if err := listener.service.DeleteContact(ctx, payload.UserEmail); err != nil { - return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot delete [contact] for user [%s] on [%s] event with ID [%s]", payload.UserID, event.Type(), event.ID())) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot delete [contact] for user [%s] on [%s] event with ID [%s]", payload.UserID, event.Type(), event.ID())) } return nil diff --git a/api/pkg/listeners/message_listener.go b/api/pkg/listeners/message_listener.go index 49fd18a3d..254bccf82 100644 --- a/api/pkg/listeners/message_listener.go +++ b/api/pkg/listeners/message_listener.go @@ -54,7 +54,7 @@ func (listener *MessageListener) OnMessagePhoneSending(ctx context.Context, even var payload events.MessagePhoneSendingPayload if err := event.DataAs(&payload); err != nil { - return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot decode [%s] into [%T]", event.Data(), payload)) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot decode [%s] into [%T]", event.Data(), payload)) } handleParams := services.HandleMessageParams{ @@ -65,7 +65,7 @@ func (listener *MessageListener) OnMessagePhoneSending(ctx context.Context, even } if err := listener.service.HandleMessageSending(ctx, handleParams); err != nil { - return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot handle sending for message with ID [%s] for event with ID [%s]", handleParams.ID, event.ID())) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot handle sending for message with ID [%s] for event with ID [%s]", handleParams.ID, event.ID())) } return nil @@ -78,7 +78,7 @@ func (listener *MessageListener) OnMessagePhoneSent(ctx context.Context, event c var payload events.MessagePhoneSentPayload if err := event.DataAs(&payload); err != nil { - return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot decode [%s] into [%T]", event.Data(), payload)) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot decode [%s] into [%T]", event.Data(), payload)) } handleParams := services.HandleMessageParams{ @@ -89,7 +89,7 @@ func (listener *MessageListener) OnMessagePhoneSent(ctx context.Context, event c } if err := listener.service.HandleMessageSent(ctx, handleParams); err != nil { - return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot handle [%s] for message with ID [%s] for event with ID [%s]", event.Type(), handleParams.ID, event.ID())) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot handle [%s] for message with ID [%s] for event with ID [%s]", event.Type(), handleParams.ID, event.ID())) } return nil } @@ -101,7 +101,7 @@ func (listener *MessageListener) OnMessagePhoneDelivered(ctx context.Context, ev var payload events.MessagePhoneDeliveredPayload if err := event.DataAs(&payload); err != nil { - return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot decode [%s] into [%T]", event.Data(), payload)) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot decode [%s] into [%T]", event.Data(), payload)) } handleParams := services.HandleMessageParams{ @@ -111,7 +111,7 @@ func (listener *MessageListener) OnMessagePhoneDelivered(ctx context.Context, ev } if err := listener.service.HandleMessageDelivered(ctx, handleParams); err != nil { - return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot handle [%s] for message with ID [%s] for event with ID [%s]", event.Type(), handleParams.ID, event.ID())) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot handle [%s] for message with ID [%s] for event with ID [%s]", event.Type(), handleParams.ID, event.ID())) } return nil @@ -124,7 +124,7 @@ func (listener *MessageListener) OnMessagePhoneFailed(ctx context.Context, event var payload events.MessageSendFailedPayload if err := event.DataAs(&payload); err != nil { - return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot decode [%s] into [%T]", event.Data(), payload)) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot decode [%s] into [%T]", event.Data(), payload)) } handleParams := services.HandleMessageFailedParams{ @@ -135,7 +135,7 @@ func (listener *MessageListener) OnMessagePhoneFailed(ctx context.Context, event } if err := listener.service.HandleMessageFailed(ctx, handleParams); err != nil { - return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot handle [%s] for message with ID [%s] for event with ID [%s]", event.Type(), handleParams.ID, event.ID())) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot handle [%s] for message with ID [%s] for event with ID [%s]", event.Type(), handleParams.ID, event.ID())) } return nil @@ -148,12 +148,12 @@ func (listener *MessageListener) onMessageNotificationFailed(ctx context.Context var payload events.MessageNotificationFailedPayload if err := event.DataAs(&payload); err != nil { - return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot decode [%s] into [%T]", event.Data(), payload)) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot decode [%s] into [%T]", event.Data(), payload)) } message, err := listener.service.GetMessage(ctx, payload.UserID, payload.MessageID) if err != nil { - return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot load message with id [%s] and user id [%s]", payload.MessageID, payload.UserID)) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot load message with id [%s] and user id [%s]", payload.MessageID, payload.UserID)) } storeParams := services.MessageStoreEventParams{ @@ -164,7 +164,7 @@ func (listener *MessageListener) onMessageNotificationFailed(ctx context.Context Source: event.Source(), } if _, err = listener.service.StoreEvent(ctx, message, storeParams); err != nil { - return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot store message event [%s] for message with ID [%s]", storeParams.EventName, storeParams.MessageID)) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot store message event [%s] for message with ID [%s]", storeParams.EventName, storeParams.MessageID)) } return nil @@ -177,7 +177,7 @@ func (listener *MessageListener) onMessageNotificationSent(ctx context.Context, var payload events.MessageNotificationSentPayload if err := event.DataAs(&payload); err != nil { - return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot decode [%s] into [%T]", event.Data(), payload)) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot decode [%s] into [%T]", event.Data(), payload)) } checkParams := services.MessageScheduleExpirationParams{ @@ -189,7 +189,7 @@ func (listener *MessageListener) onMessageNotificationSent(ctx context.Context, MessageExpirationDuration: payload.MessageExpirationDuration, } if err := listener.service.ScheduleExpirationCheck(ctx, checkParams); err != nil { - return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot exchedule expiration check for ID [%s] and userID [%s]", checkParams.MessageID, checkParams.UserID)) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot exchedule expiration check for ID [%s] and userID [%s]", checkParams.MessageID, checkParams.UserID)) } handleParams := services.HandleMessageParams{ @@ -199,7 +199,7 @@ func (listener *MessageListener) onMessageNotificationSent(ctx context.Context, Timestamp: payload.NotificationSentAt, } if err := listener.service.HandleMessageNotificationSent(ctx, handleParams); err != nil { - return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot handle event [%s] for message [%s] and userID [%s]", event.Type(), checkParams.MessageID, checkParams.UserID)) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot handle event [%s] for message [%s] and userID [%s]", event.Type(), checkParams.MessageID, checkParams.UserID)) } return nil @@ -212,7 +212,7 @@ func (listener *MessageListener) onMessageSendExpiredCheck(ctx context.Context, var payload events.MessageSendExpiredCheckPayload if err := event.DataAs(&payload); err != nil { - return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot decode [%s] into [%T]", event.Data(), payload)) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot decode [%s] into [%T]", event.Data(), payload)) } checkParams := services.MessageCheckExpired{ @@ -221,7 +221,7 @@ func (listener *MessageListener) onMessageSendExpiredCheck(ctx context.Context, Source: event.Source(), } if err := listener.service.CheckExpired(ctx, checkParams); err != nil { - return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot check expiration for message with ID [%s] and userID [%s]", checkParams.MessageID, checkParams.UserID)) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot check expiration for message with ID [%s] and userID [%s]", checkParams.MessageID, checkParams.UserID)) } return nil @@ -234,7 +234,7 @@ func (listener *MessageListener) onMessageSendExpired(ctx context.Context, event var payload events.MessageSendExpiredPayload if err := event.DataAs(&payload); err != nil { - return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot decode [%s] into [%T]", event.Data(), payload)) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot decode [%s] into [%T]", event.Data(), payload)) } expiredParams := services.HandleMessageParams{ @@ -244,7 +244,7 @@ func (listener *MessageListener) onMessageSendExpired(ctx context.Context, event Timestamp: payload.Timestamp, } if err := listener.service.HandleMessageExpired(ctx, expiredParams); err != nil { - return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot handle event [%s] for ID [%s] and userID [%s]", event.Type(), expiredParams.ID, expiredParams.UserID)) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot handle event [%s] for ID [%s] and userID [%s]", event.Type(), expiredParams.ID, expiredParams.UserID)) } return nil @@ -257,7 +257,7 @@ func (listener *MessageListener) onMessageNotificationScheduled(ctx context.Cont var payload events.MessageNotificationScheduledPayload if err := event.DataAs(&payload); err != nil { - return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot decode [%s] into [%T]", event.Data(), payload)) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot decode [%s] into [%T]", event.Data(), payload)) } expiredParams := services.HandleMessageParams{ @@ -267,7 +267,7 @@ func (listener *MessageListener) onMessageNotificationScheduled(ctx context.Cont Timestamp: payload.ScheduledAt, } if err := listener.service.HandleMessageNotificationScheduled(ctx, expiredParams); err != nil { - return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot handle event [%s] for ID [%s] and userID [%s]", event.Type(), expiredParams.ID, expiredParams.UserID)) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot handle event [%s] for ID [%s] and userID [%s]", event.Type(), expiredParams.ID, expiredParams.UserID)) } return nil @@ -280,11 +280,11 @@ func (listener *MessageListener) onMessageThreadAPIDeleted(ctx context.Context, var payload events.MessageThreadAPIDeletedPayload if err := event.DataAs(&payload); err != nil { - return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot decode [%s] into [%T]", event.Data(), payload)) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot decode [%s] into [%T]", event.Data(), payload)) } if err := listener.service.DeleteByOwnerAndContact(ctx, payload.UserID, payload.Owner, payload.Contact); err != nil { - return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot handle [%s] event with ID [%s] and userID [%s]", event.Type(), event.ID(), payload.UserID)) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot handle [%s] event with ID [%s] and userID [%s]", event.Type(), event.ID(), payload.UserID)) } return nil @@ -297,11 +297,11 @@ func (listener *MessageListener) onMessageCallMissed(ctx context.Context, event payload := new(events.MessageCallMissedPayload) if err := event.DataAs(payload); err != nil { - return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot decode [%s] into [%T]", event.Data(), payload)) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot decode [%s] into [%T]", event.Data(), payload)) } if err := listener.service.RespondToMissedCall(ctx, event.Source(), payload); err != nil { - return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot handle [%s] event with ID [%s] and userID [%s]", event.Type(), event.ID(), payload.UserID)) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot handle [%s] event with ID [%s] and userID [%s]", event.Type(), event.ID(), payload.UserID)) } return nil @@ -313,11 +313,11 @@ func (listener *MessageListener) onUserAccountDeleted(ctx context.Context, event var payload events.UserAccountDeletedPayload if err := event.DataAs(&payload); err != nil { - return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot decode [%s] into [%T]", event.Data(), payload)) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot decode [%s] into [%T]", event.Data(), payload)) } if err := listener.service.DeleteAllForUser(ctx, payload.UserID); err != nil { - return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot delete [entities.Message] for user [%s] on [%s] event with ID [%s]", payload.UserID, event.Type(), event.ID())) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot delete [entities.Message] for user [%s] on [%s] event with ID [%s]", payload.UserID, event.Type(), event.ID())) } return nil diff --git a/api/pkg/listeners/message_send_schedule_listener.go b/api/pkg/listeners/message_send_schedule_listener.go index 59f93b847..8e2838c72 100644 --- a/api/pkg/listeners/message_send_schedule_listener.go +++ b/api/pkg/listeners/message_send_schedule_listener.go @@ -45,11 +45,11 @@ func (listener *MessageSendScheduleListener) onUserAccountDeleted( var payload events.UserAccountDeletedPayload if err := event.DataAs(&payload); err != nil { - return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot decode [%s] into [%T]", event.Data(), payload)) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot decode [%s] into [%T]", event.Data(), payload)) } if err := listener.service.DeleteAllForUser(ctx, payload.UserID); err != nil { - return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot delete [entities.MessageSendSchedule] for user [%s] on [%s] event with ID [%s]", payload.UserID, event.Type(), event.ID())) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot delete [entities.MessageSendSchedule] for user [%s] on [%s] event with ID [%s]", payload.UserID, event.Type(), event.ID())) } return nil diff --git a/api/pkg/listeners/message_thread_listener.go b/api/pkg/listeners/message_thread_listener.go index ad22e810f..f6d2c52c6 100644 --- a/api/pkg/listeners/message_thread_listener.go +++ b/api/pkg/listeners/message_thread_listener.go @@ -54,7 +54,7 @@ func (listener *MessageThreadListener) OnMessageAPISent(ctx context.Context, eve var payload events.MessageAPISentPayload if err := event.DataAs(&payload); err != nil { - return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot decode [%s] into [%T]", event.Data(), payload)) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot decode [%s] into [%T]", event.Data(), payload)) } updateParams := services.MessageThreadUpdateParams{ @@ -68,7 +68,7 @@ func (listener *MessageThreadListener) OnMessageAPISent(ctx context.Context, eve } if err := listener.service.UpdateThread(ctx, updateParams); err != nil { - return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot update thread for message with ID [%s] for event with ID [%s]", updateParams.MessageID, event.ID())) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot update thread for message with ID [%s] for event with ID [%s]", updateParams.MessageID, event.ID())) } return nil @@ -81,11 +81,11 @@ func (listener *MessageThreadListener) onMessageDeleted(ctx context.Context, eve payload := new(events.MessageAPIDeletedPayload) if err := event.DataAs(payload); err != nil { - return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot decode [%s] into [%T]", event.Data(), payload)) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot decode [%s] into [%T]", event.Data(), payload)) } if err := listener.service.UpdateAfterDeletedMessage(ctx, payload); err != nil { - return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot update thread for message with ID [%s] for event with ID [%s]", payload.MessageID, event.ID())) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot update thread for message with ID [%s] for event with ID [%s]", payload.MessageID, event.ID())) } return nil @@ -98,7 +98,7 @@ func (listener *MessageThreadListener) OnMessagePhoneSending(ctx context.Context var payload events.MessagePhoneSendingPayload if err := event.DataAs(&payload); err != nil { - return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot decode [%s] into [%T]", event.Data(), payload)) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot decode [%s] into [%T]", event.Data(), payload)) } updateParams := services.MessageThreadUpdateParams{ @@ -112,7 +112,7 @@ func (listener *MessageThreadListener) OnMessagePhoneSending(ctx context.Context } if err := listener.service.UpdateThread(ctx, updateParams); err != nil { - return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot update thread for message with ID [%s] for event with ID [%s]", updateParams.MessageID, event.ID())) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot update thread for message with ID [%s] for event with ID [%s]", updateParams.MessageID, event.ID())) } return nil @@ -125,7 +125,7 @@ func (listener *MessageThreadListener) OnMessagePhoneSent(ctx context.Context, e var payload events.MessagePhoneSentPayload if err := event.DataAs(&payload); err != nil { - return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot decode [%s] into [%T]", event.Data(), payload)) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot decode [%s] into [%T]", event.Data(), payload)) } updateParams := services.MessageThreadUpdateParams{ @@ -139,7 +139,7 @@ func (listener *MessageThreadListener) OnMessagePhoneSent(ctx context.Context, e } if err := listener.service.UpdateThread(ctx, updateParams); err != nil { - return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot update thread for message with ID [%s] for event with ID [%s]", updateParams.MessageID, event.ID())) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot update thread for message with ID [%s] for event with ID [%s]", updateParams.MessageID, event.ID())) } return nil @@ -152,7 +152,7 @@ func (listener *MessageThreadListener) OnMessagePhoneDelivered(ctx context.Conte var payload events.MessagePhoneDeliveredPayload if err := event.DataAs(&payload); err != nil { - return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot decode [%s] into [%T]", event.Data(), payload)) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot decode [%s] into [%T]", event.Data(), payload)) } updateParams := services.MessageThreadUpdateParams{ @@ -166,7 +166,7 @@ func (listener *MessageThreadListener) OnMessagePhoneDelivered(ctx context.Conte } if err := listener.service.UpdateThread(ctx, updateParams); err != nil { - return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot update thread for message with ID [%s] for event with ID [%s]", updateParams.MessageID, event.ID())) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot update thread for message with ID [%s] for event with ID [%s]", updateParams.MessageID, event.ID())) } return nil @@ -179,7 +179,7 @@ func (listener *MessageThreadListener) OnMessagePhoneFailed(ctx context.Context, var payload events.MessageSendFailedPayload if err := event.DataAs(&payload); err != nil { - return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot decode [%s] into [%T] for event [%s]", event.Data(), payload, event.ID())) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot decode [%s] into [%T] for event [%s]", event.Data(), payload, event.ID())) } updateParams := services.MessageThreadUpdateParams{ @@ -193,7 +193,7 @@ func (listener *MessageThreadListener) OnMessagePhoneFailed(ctx context.Context, } if err := listener.service.UpdateThread(ctx, updateParams); err != nil { - return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot update thread for message with ID [%s] for event with ID [%s]", updateParams.MessageID, event.ID())) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot update thread for message with ID [%s] for event with ID [%s]", updateParams.MessageID, event.ID())) } return nil @@ -206,7 +206,7 @@ func (listener *MessageThreadListener) OnMessagePhoneReceived(ctx context.Contex var payload events.MessagePhoneReceivedPayload if err := event.DataAs(&payload); err != nil { - return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot decode [%s] into [%T]", event.Data(), payload)) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot decode [%s] into [%T]", event.Data(), payload)) } updateParams := services.MessageThreadUpdateParams{ @@ -222,7 +222,7 @@ func (listener *MessageThreadListener) OnMessagePhoneReceived(ctx context.Contex } if err := listener.service.UpdateThread(ctx, updateParams); err != nil { - return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot update thread for message with ID [%s] for event with ID [%s]", updateParams.MessageID, event.ID())) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot update thread for message with ID [%s] for event with ID [%s]", updateParams.MessageID, event.ID())) } return nil @@ -235,7 +235,7 @@ func (listener *MessageThreadListener) OnMessageCallMissed(ctx context.Context, var payload events.MessageCallMissedPayload if err := event.DataAs(&payload); err != nil { - return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot decode [%s] into [%T]", event.Data(), payload)) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot decode [%s] into [%T]", event.Data(), payload)) } params := services.MessageThreadUpdateParams{ @@ -250,7 +250,7 @@ func (listener *MessageThreadListener) OnMessageCallMissed(ctx context.Context, EventTimestamp: event.Time(), } if err := listener.service.UpdateThread(ctx, params); err != nil { - return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot update thread for missed call [%s] on event [%s]", payload.MessageID, event.ID())) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot update thread for missed call [%s] on event [%s]", payload.MessageID, event.ID())) } return nil } @@ -262,7 +262,7 @@ func (listener *MessageThreadListener) onMessageNotificationScheduled(ctx contex var payload events.MessageNotificationScheduledPayload if err := event.DataAs(&payload); err != nil { - return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot decode [%s] into [%T]", event.Data(), payload)) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot decode [%s] into [%T]", event.Data(), payload)) } updateParams := services.MessageThreadUpdateParams{ @@ -276,7 +276,7 @@ func (listener *MessageThreadListener) onMessageNotificationScheduled(ctx contex } if err := listener.service.UpdateThread(ctx, updateParams); err != nil { - return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot update thread for message with ID [%s] for event with ID [%s]", updateParams.MessageID, event.ID())) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot update thread for message with ID [%s] for event with ID [%s]", updateParams.MessageID, event.ID())) } return nil @@ -289,7 +289,7 @@ func (listener *MessageThreadListener) onMessageExpired(ctx context.Context, eve var payload events.MessageSendExpiredPayload if err := event.DataAs(&payload); err != nil { - return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot decode [%s] into [%T]", event.Data(), payload)) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot decode [%s] into [%T]", event.Data(), payload)) } updateParams := services.MessageThreadUpdateParams{ @@ -303,7 +303,7 @@ func (listener *MessageThreadListener) onMessageExpired(ctx context.Context, eve } if err := listener.service.UpdateThread(ctx, updateParams); err != nil { - return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot update thread for message with ID [%s] for event with ID [%s]", updateParams.MessageID, event.ID())) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot update thread for message with ID [%s] for event with ID [%s]", updateParams.MessageID, event.ID())) } return nil @@ -315,11 +315,11 @@ func (listener *MessageThreadListener) onUserAccountDeleted(ctx context.Context, var payload events.UserAccountDeletedPayload if err := event.DataAs(&payload); err != nil { - return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot decode [%s] into [%T]", event.Data(), payload)) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot decode [%s] into [%T]", event.Data(), payload)) } if err := listener.service.DeleteAllForUser(ctx, payload.UserID); err != nil { - return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot delete [entities.MessageThread] for user [%s] on [%s] event with ID [%s]", payload.UserID, event.Type(), event.ID())) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot delete [entities.MessageThread] for user [%s] on [%s] event with ID [%s]", payload.UserID, event.Type(), event.ID())) } return nil diff --git a/api/pkg/listeners/phone_api_key_listener.go b/api/pkg/listeners/phone_api_key_listener.go index 45333ac56..8eb3621e0 100644 --- a/api/pkg/listeners/phone_api_key_listener.go +++ b/api/pkg/listeners/phone_api_key_listener.go @@ -48,7 +48,7 @@ func (listener *PhoneAPIKeyListener) onPhoneUpdated(ctx context.Context, event c var payload events.PhoneUpdatedPayload if err := event.DataAs(&payload); err != nil { - return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot decode [%s] into [%T]", event.Data(), payload)) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot decode [%s] into [%T]", event.Data(), payload)) } if payload.PhoneAPIKeyID == nil { @@ -57,7 +57,7 @@ func (listener *PhoneAPIKeyListener) onPhoneUpdated(ctx context.Context, event c } if err := listener.service.AddPhone(ctx, payload.UserID, *payload.PhoneAPIKeyID, payload.PhoneID); err != nil { - return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot store heartbeat monitor with params [%s] for event with ID [%s]", spew.Sdump(payload), event.ID())) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot store heartbeat monitor with params [%s] for event with ID [%s]", spew.Sdump(payload), event.ID())) } return nil @@ -70,11 +70,11 @@ func (listener *PhoneAPIKeyListener) onPhoneDeleted(ctx context.Context, event c var payload events.PhoneDeletedPayload if err := event.DataAs(&payload); err != nil { - return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot decode [%s] into [%T]", event.Data(), payload)) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot decode [%s] into [%T]", event.Data(), payload)) } if err := listener.service.RemovePhoneByID(ctx, payload.UserID, payload.PhoneID, payload.Owner); err != nil { - return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot remove phone with ID [%s] from phone api key for [%s] event with ID [%s]", payload.PhoneID, event.Type(), event.ID())) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot remove phone with ID [%s] from phone api key for [%s] event with ID [%s]", payload.PhoneID, event.Type(), event.ID())) } return nil @@ -87,11 +87,11 @@ func (listener *PhoneAPIKeyListener) onUserAccountDeleted(ctx context.Context, e var payload events.UserAccountDeletedPayload if err := event.DataAs(&payload); err != nil { - return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot decode [%s] into [%T]", event.Data(), payload)) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot decode [%s] into [%T]", event.Data(), payload)) } if err := listener.service.DeleteAllForUser(ctx, payload.UserID); err != nil { - return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot delete all [%T] for user with ID [%s] for [%s] event with ID [%s]", entities.PhoneAPIKey{}, payload.UserID, event.Type(), event.ID())) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot delete all [%T] for user with ID [%s] for [%s] event with ID [%s]", entities.PhoneAPIKey{}, payload.UserID, event.Type(), event.ID())) } return nil diff --git a/api/pkg/listeners/phone_listener.go b/api/pkg/listeners/phone_listener.go index f39448483..6f3418c80 100644 --- a/api/pkg/listeners/phone_listener.go +++ b/api/pkg/listeners/phone_listener.go @@ -44,11 +44,11 @@ func (listener *PhoneListener) onMessageSendScheduleDeleted(ctx context.Context, var payload events.MessageSendScheduleDeletedPayload if err := event.DataAs(&payload); err != nil { - return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot decode [%s] into [%T]", event.Data(), payload)) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot decode [%s] into [%T]", event.Data(), payload)) } if err := listener.service.NullifyScheduleID(ctx, payload.UserID, payload.ScheduleID); err != nil { - return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot nullify schedule ID [%s] for user [%s] on [%s] event with ID [%s]", payload.ScheduleID, payload.UserID, event.Type(), event.ID())) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot nullify schedule ID [%s] for user [%s] on [%s] event with ID [%s]", payload.ScheduleID, payload.UserID, event.Type(), event.ID())) } return nil @@ -61,11 +61,11 @@ func (listener *PhoneListener) onUserAccountDeleted(ctx context.Context, event c var payload events.UserAccountDeletedPayload if err := event.DataAs(&payload); err != nil { - return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot decode [%s] into [%T]", event.Data(), payload)) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot decode [%s] into [%T]", event.Data(), payload)) } if err := listener.service.DeleteAllForUser(ctx, payload.UserID); err != nil { - return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot delete all [entities.Phone] for user [%s] on [%s] event with ID [%s]", payload.UserID, event.Type(), event.ID())) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot delete all [entities.Phone] for user [%s] on [%s] event with ID [%s]", payload.UserID, event.Type(), event.ID())) } return nil diff --git a/api/pkg/listeners/phone_notification_listener.go b/api/pkg/listeners/phone_notification_listener.go index 40452a946..5bac8d73e 100644 --- a/api/pkg/listeners/phone_notification_listener.go +++ b/api/pkg/listeners/phone_notification_listener.go @@ -49,7 +49,7 @@ func (listener *PhoneNotificationListener) onMessageAPISent(ctx context.Context, var payload events.MessageAPISentPayload if err := event.DataAs(&payload); err != nil { - return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot decode [%s] into [%T]", event.Data(), payload)) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot decode [%s] into [%T]", event.Data(), payload)) } sendParams := &services.PhoneNotificationScheduleParams{ @@ -66,7 +66,7 @@ func (listener *PhoneNotificationListener) onMessageAPISent(ctx context.Context, } if err := listener.service.Schedule(ctx, sendParams); err != nil { - return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot send notification with params [%s] for event with ID [%s]", spew.Sdump(sendParams), event.ID())) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot send notification with params [%s] for event with ID [%s]", spew.Sdump(sendParams), event.ID())) } return nil @@ -79,7 +79,7 @@ func (listener *PhoneNotificationListener) onMessageSendRetry(ctx context.Contex var payload events.MessageSendRetryPayload if err := event.DataAs(&payload); err != nil { - return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot decode [%s] into [%T]", event.Data(), payload)) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot decode [%s] into [%T]", event.Data(), payload)) } sendParams := &services.PhoneNotificationScheduleParams{ @@ -94,7 +94,7 @@ func (listener *PhoneNotificationListener) onMessageSendRetry(ctx context.Contex } if err := listener.service.Schedule(ctx, sendParams); err != nil { - return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot send notification with params [%s] for event with ID [%s]", spew.Sdump(sendParams), event.ID())) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot send notification with params [%s] for event with ID [%s]", spew.Sdump(sendParams), event.ID())) } return nil @@ -107,11 +107,11 @@ func (listener *PhoneNotificationListener) onPhoneHeartbeatMissed(ctx context.Co payload := new(events.PhoneHeartbeatMissedPayload) if err := event.DataAs(payload); err != nil { - return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot decode [%s] into [%T]", event.Data(), payload)) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot decode [%s] into [%T]", event.Data(), payload)) } if err := listener.service.SendHeartbeatFCM(ctx, payload); err != nil { - return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot schedule send heartbeat FCM with params [%s] for event with ID [%s]", spew.Sdump(payload), event.ID())) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot schedule send heartbeat FCM with params [%s] for event with ID [%s]", spew.Sdump(payload), event.ID())) } return nil @@ -124,7 +124,7 @@ func (listener *PhoneNotificationListener) onMessageNotificationSend(ctx context var payload events.MessageNotificationSendPayload if err := event.DataAs(&payload); err != nil { - return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot decode [%s] into [%T]", event.Data(), payload)) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot decode [%s] into [%T]", event.Data(), payload)) } scheduleParams := &services.PhoneNotificationSendParams{ @@ -137,7 +137,7 @@ func (listener *PhoneNotificationListener) onMessageNotificationSend(ctx context } if err := listener.service.Send(ctx, scheduleParams); err != nil { - return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot schedule notification with params [%s] for event with ID [%s]", spew.Sdump(scheduleParams), event.ID())) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot schedule notification with params [%s] for event with ID [%s]", spew.Sdump(scheduleParams), event.ID())) } return nil @@ -149,11 +149,11 @@ func (listener *PhoneNotificationListener) onUserAccountDeleted(ctx context.Cont var payload events.UserAccountDeletedPayload if err := event.DataAs(&payload); err != nil { - return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot decode [%s] into [%T]", event.Data(), payload)) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot decode [%s] into [%T]", event.Data(), payload)) } if err := listener.service.DeleteAllForUser(ctx, payload.UserID); err != nil { - return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot delete [entities.Phone] for user [%s] on [%s] event with ID [%s]", payload.UserID, event.Type(), event.ID())) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot delete [entities.Phone] for user [%s] on [%s] event with ID [%s]", payload.UserID, event.Type(), event.ID())) } return nil @@ -166,11 +166,11 @@ func (listener *PhoneNotificationListener) onMessageAPIDeleted(ctx context.Conte var payload events.MessageAPIDeletedPayload if err := event.DataAs(&payload); err != nil { - return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot decode [%s] into [%T]", event.Data(), payload)) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot decode [%s] into [%T]", event.Data(), payload)) } if err := listener.service.DeleteByMessageID(ctx, payload.UserID, payload.MessageID); err != nil { - return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot delete [entities.PhoneNotification] for user [%s] and message [%s] on [%s] event with ID [%s]", payload.UserID, payload.MessageID, event.Type(), event.ID())) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot delete [entities.PhoneNotification] for user [%s] and message [%s] on [%s] event with ID [%s]", payload.UserID, payload.MessageID, event.Type(), event.ID())) } return nil diff --git a/api/pkg/listeners/user_listener.go b/api/pkg/listeners/user_listener.go index e4a15d32f..f681cb1ca 100644 --- a/api/pkg/listeners/user_listener.go +++ b/api/pkg/listeners/user_listener.go @@ -50,7 +50,7 @@ func (listener *UserListener) onPhoneHeartbeatDead(ctx context.Context, event cl var payload events.PhoneHeartbeatOfflinePayload if err := event.DataAs(&payload); err != nil { - return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot decode [%s] into [%T]", event.Data(), payload)) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot decode [%s] into [%T]", event.Data(), payload)) } sendParams := &services.UserSendPhoneDeadEmailParams{ @@ -61,7 +61,7 @@ func (listener *UserListener) onPhoneHeartbeatDead(ctx context.Context, event cl } if err := listener.service.SendPhoneDeadEmail(ctx, sendParams); err != nil { - return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot send notification with params [%s] for event with ID [%s]", spew.Sdump(sendParams), event.ID())) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot send notification with params [%s] for event with ID [%s]", spew.Sdump(sendParams), event.ID())) } return nil @@ -74,11 +74,11 @@ func (listener *UserListener) onUserAPIKeyRotated(ctx context.Context, event clo payload := new(events.UserAPIKeyRotatedPayload) if err := event.DataAs(&payload); err != nil { - return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot decode [%s] into [%T]", event.Data(), payload)) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot decode [%s] into [%T]", event.Data(), payload)) } if err := listener.service.SendAPIKeyRotatedEmail(ctx, payload); err != nil { - return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot send notification with params [%s] for event with ID [%s]", spew.Sdump(payload), event.ID())) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot send notification with params [%s] for event with ID [%s]", spew.Sdump(payload), event.ID())) } return nil @@ -91,11 +91,11 @@ func (listener *UserListener) OnUserSubscriptionCreated(ctx context.Context, eve var payload events.UserSubscriptionCreatedPayload if err := event.DataAs(&payload); err != nil { - return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot decode [%s] into [%T]", event.Data(), payload)) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot decode [%s] into [%T]", event.Data(), payload)) } if err := listener.service.StartSubscription(ctx, &payload); err != nil { - return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot start subscription for user with ID [%s] for event with ID [%s]", payload.UserID, event.ID())) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot start subscription for user with ID [%s] for event with ID [%s]", payload.UserID, event.ID())) } return nil @@ -108,11 +108,11 @@ func (listener *UserListener) OnUserSubscriptionCancelled(ctx context.Context, e var payload events.UserSubscriptionCancelledPayload if err := event.DataAs(&payload); err != nil { - return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot decode [%s] into [%T]", event.Data(), payload)) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot decode [%s] into [%T]", event.Data(), payload)) } if err := listener.service.CancelSubscription(ctx, &payload); err != nil { - return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot cancell subscription for user with ID [%s] for event with ID [%s]", payload.UserID, event.ID())) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot cancell subscription for user with ID [%s] for event with ID [%s]", payload.UserID, event.ID())) } return nil @@ -125,11 +125,11 @@ func (listener *UserListener) OnUserSubscriptionExpired(ctx context.Context, eve var payload events.UserSubscriptionExpiredPayload if err := event.DataAs(&payload); err != nil { - return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot decode [%s] into [%T]", event.Data(), payload)) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot decode [%s] into [%T]", event.Data(), payload)) } if err := listener.service.ExpireSubscription(ctx, &payload); err != nil { - return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot expire subscription for user with ID [%s] for event with ID [%s]", payload.UserID, event.ID())) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot expire subscription for user with ID [%s] for event with ID [%s]", payload.UserID, event.ID())) } return nil @@ -142,11 +142,11 @@ func (listener *UserListener) OnUserSubscriptionUpdated(ctx context.Context, eve var payload events.UserSubscriptionUpdatedPayload if err := event.DataAs(&payload); err != nil { - return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot decode [%s] into [%T]", event.Data(), payload)) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot decode [%s] into [%T]", event.Data(), payload)) } if err := listener.service.UpdateSubscription(ctx, &payload); err != nil { - return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot expire subscription for user with ID [%s] for event with ID [%s]", payload.UserID, event.ID())) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot expire subscription for user with ID [%s] for event with ID [%s]", payload.UserID, event.ID())) } return nil @@ -158,11 +158,11 @@ func (listener *UserListener) onUserAccountDeleted(ctx context.Context, event cl var payload events.UserAccountDeletedPayload if err := event.DataAs(&payload); err != nil { - return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot decode [%s] into [%T]", event.Data(), payload)) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot decode [%s] into [%T]", event.Data(), payload)) } if err := listener.service.DeleteAuthUser(ctx, payload.UserID); err != nil { - return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot delete [entities.AuthUser] for user [%s] on [%s] event with ID [%s]", payload.UserID, event.Type(), event.ID())) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot delete [entities.AuthUser] for user [%s] on [%s] event with ID [%s]", payload.UserID, event.Type(), event.ID())) } return nil diff --git a/api/pkg/listeners/webhook_listener.go b/api/pkg/listeners/webhook_listener.go index ff9ab5700..d14008b13 100644 --- a/api/pkg/listeners/webhook_listener.go +++ b/api/pkg/listeners/webhook_listener.go @@ -50,11 +50,11 @@ func (listener *WebhookListener) OnMessagePhoneReceived(ctx context.Context, eve var payload events.MessagePhoneReceivedPayload if err := event.DataAs(&payload); err != nil { - return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot decode [%s] into [%T]", event.Data(), payload)) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot decode [%s] into [%T]", event.Data(), payload)) } if err := listener.service.Send(ctx, payload.UserID, event, payload.Owner); err != nil { - return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot process [%s] event with ID [%s]", event.Type(), event.ID())) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot process [%s] event with ID [%s]", event.Type(), event.ID())) } return nil @@ -67,11 +67,11 @@ func (listener *WebhookListener) OnMessageSendExpired(ctx context.Context, event var payload events.MessageSendExpiredPayload if err := event.DataAs(&payload); err != nil { - return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot decode [%s] into [%T]", event.Data(), payload)) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot decode [%s] into [%T]", event.Data(), payload)) } if err := listener.service.Send(ctx, payload.UserID, event, payload.Owner); err != nil { - return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot process [%s] event with ID [%s]", event.Type(), event.ID())) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot process [%s] event with ID [%s]", event.Type(), event.ID())) } return nil @@ -84,11 +84,11 @@ func (listener *WebhookListener) OnMessageSendFailed(ctx context.Context, event var payload events.MessageSendFailedPayload if err := event.DataAs(&payload); err != nil { - return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot decode [%s] into [%T]", event.Data(), payload)) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot decode [%s] into [%T]", event.Data(), payload)) } if err := listener.service.Send(ctx, payload.UserID, event, payload.Owner); err != nil { - return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot process [%s] event with ID [%s]", event.Type(), event.ID())) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot process [%s] event with ID [%s]", event.Type(), event.ID())) } return nil @@ -101,11 +101,11 @@ func (listener *WebhookListener) OnMessagePhoneSent(ctx context.Context, event c var payload events.MessagePhoneSentPayload if err := event.DataAs(&payload); err != nil { - return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot decode [%s] into [%T]", event.Data(), payload)) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot decode [%s] into [%T]", event.Data(), payload)) } if err := listener.service.Send(ctx, payload.UserID, event, payload.Owner); err != nil { - return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot process [%s] event with ID [%s]", event.Type(), event.ID())) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot process [%s] event with ID [%s]", event.Type(), event.ID())) } return nil @@ -118,11 +118,11 @@ func (listener *WebhookListener) OnMessagePhoneDelivered(ctx context.Context, ev var payload events.MessagePhoneDeliveredPayload if err := event.DataAs(&payload); err != nil { - return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot decode [%s] into [%T]", event.Data(), payload)) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot decode [%s] into [%T]", event.Data(), payload)) } if err := listener.service.Send(ctx, payload.UserID, event, payload.Owner); err != nil { - return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot process [%s] event with ID [%s]", event.Type(), event.ID())) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot process [%s] event with ID [%s]", event.Type(), event.ID())) } return nil @@ -135,11 +135,11 @@ func (listener *WebhookListener) onPhoneHeartbeatOffline(ctx context.Context, ev var payload events.PhoneHeartbeatOfflinePayload if err := event.DataAs(&payload); err != nil { - return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot decode [%s] into [%T]", event.Data(), payload)) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot decode [%s] into [%T]", event.Data(), payload)) } if err := listener.service.Send(ctx, payload.UserID, event, payload.Owner); err != nil { - return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot process [%s] event with ID [%s]", event.Type(), event.ID())) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot process [%s] event with ID [%s]", event.Type(), event.ID())) } return nil @@ -152,11 +152,11 @@ func (listener *WebhookListener) onPhoneHeartbeatOnline(ctx context.Context, eve var payload events.PhoneHeartbeatOnlinePayload if err := event.DataAs(&payload); err != nil { - return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot decode [%s] into [%T]", event.Data(), payload)) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot decode [%s] into [%T]", event.Data(), payload)) } if err := listener.service.Send(ctx, payload.UserID, event, payload.Owner); err != nil { - return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot process [%s] event with ID [%s]", event.Type(), event.ID())) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot process [%s] event with ID [%s]", event.Type(), event.ID())) } return nil @@ -169,11 +169,11 @@ func (listener *WebhookListener) onMessageCallMissed(ctx context.Context, event var payload events.MessageCallMissedPayload if err := event.DataAs(&payload); err != nil { - return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot decode [%s] into [%T]", event.Data(), payload)) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot decode [%s] into [%T]", event.Data(), payload)) } if err := listener.service.Send(ctx, payload.UserID, event, payload.Owner); err != nil { - return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot process [%s] event with ID [%s]", event.Type(), event.ID())) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot process [%s] event with ID [%s]", event.Type(), event.ID())) } return nil @@ -185,11 +185,11 @@ func (listener *WebhookListener) onUserAccountDeleted(ctx context.Context, event var payload events.UserAccountDeletedPayload if err := event.DataAs(&payload); err != nil { - return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot decode [%s] into [%T]", event.Data(), payload)) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot decode [%s] into [%T]", event.Data(), payload)) } if err := listener.service.DeleteAllForUser(ctx, payload.UserID); err != nil { - return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot delete [entities.Webhook] for user [%s] on [%s] event with ID [%s]", payload.UserID, event.Type(), event.ID())) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot delete [entities.Webhook] for user [%s] on [%s] event with ID [%s]", payload.UserID, event.Type(), event.ID())) } return nil diff --git a/api/pkg/listeners/websocket_listener.go b/api/pkg/listeners/websocket_listener.go index b06381d78..bdc43f938 100644 --- a/api/pkg/listeners/websocket_listener.go +++ b/api/pkg/listeners/websocket_listener.go @@ -46,11 +46,11 @@ func (listener *WebsocketListener) onMessageCallMissed(ctx context.Context, even var payload events.MessageCallMissedPayload if err := event.DataAs(&payload); err != nil { - return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot decode [%s] into [%T]", event.Data(), payload)) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot decode [%s] into [%T]", event.Data(), payload)) } if err := listener.client.Trigger(payload.UserID.String(), event.Type(), event.ID()); err != nil { - return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot trigger websocket [%s] event with ID [%s] for user with ID [%s]", event.Type(), event.ID(), payload.UserID)) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot trigger websocket [%s] event with ID [%s] for user with ID [%s]", event.Type(), event.ID(), payload.UserID)) } return nil } @@ -62,11 +62,11 @@ func (listener *WebsocketListener) onMessagePhoneSent(ctx context.Context, event var payload events.MessagePhoneSentPayload if err := event.DataAs(&payload); err != nil { - return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot decode [%s] into [%T]", event.Data(), payload)) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot decode [%s] into [%T]", event.Data(), payload)) } if err := listener.client.Trigger(payload.UserID.String(), event.Type(), event.ID()); err != nil { - return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot trigger websocket [%s] event with ID [%s] for user with ID [%s]", event.Type(), event.ID(), payload.UserID)) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot trigger websocket [%s] event with ID [%s] for user with ID [%s]", event.Type(), event.ID(), payload.UserID)) } return nil @@ -79,11 +79,11 @@ func (listener *WebsocketListener) onMessagePhoneReceived(ctx context.Context, e var payload events.MessagePhoneReceivedPayload if err := event.DataAs(&payload); err != nil { - return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot decode [%s] into [%T]", event.Data(), payload)) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot decode [%s] into [%T]", event.Data(), payload)) } if err := listener.client.Trigger(payload.UserID.String(), event.Type(), event.ID()); err != nil { - return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot trigger websocket [%s] event with ID [%s] for user with ID [%s]", event.Type(), event.ID(), payload.UserID)) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot trigger websocket [%s] event with ID [%s] for user with ID [%s]", event.Type(), event.ID(), payload.UserID)) } return nil @@ -96,11 +96,11 @@ func (listener *WebsocketListener) onMessagePhoneFailed(ctx context.Context, eve var payload events.MessageSendFailedPayload if err := event.DataAs(&payload); err != nil { - return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot decode [%s] into [%T]", event.Data(), payload)) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot decode [%s] into [%T]", event.Data(), payload)) } if err := listener.client.Trigger(payload.UserID.String(), event.Type(), event.ID()); err != nil { - return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot trigger websocket [%s] event with ID [%s] for user with ID [%s]", event.Type(), event.ID(), payload.UserID)) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot trigger websocket [%s] event with ID [%s] for user with ID [%s]", event.Type(), event.ID(), payload.UserID)) } return nil @@ -113,11 +113,11 @@ func (listener *WebsocketListener) onPhoneUpdated(ctx context.Context, event clo var payload events.PhoneUpdatedPayload if err := event.DataAs(&payload); err != nil { - return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot decode [%s] into [%T]", event.Data(), payload)) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot decode [%s] into [%T]", event.Data(), payload)) } if err := listener.client.Trigger(payload.UserID.String(), event.Type(), event.ID()); err != nil { - return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot trigger websocket [%s] event with ID [%s] for user with ID [%s]", event.Type(), event.ID(), payload.UserID)) + return listener.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot trigger websocket [%s] event with ID [%s] for user with ID [%s]", event.Type(), event.ID(), payload.UserID)) } return nil diff --git a/api/pkg/middlewares/api_key_auth_middleware.go b/api/pkg/middlewares/api_key_auth_middleware.go index f2b184194..cec70976e 100644 --- a/api/pkg/middlewares/api_key_auth_middleware.go +++ b/api/pkg/middlewares/api_key_auth_middleware.go @@ -28,7 +28,7 @@ func APIKeyAuth(logger telemetry.Logger, tracer telemetry.Tracer, userRepository authUser, err := userRepository.LoadAuthContext(ctx, apiKey) if err != nil { - ctxLogger.Error(stacktrace.Propagate(err, "cannot load user with api key [%s]", apiKey)) + ctxLogger.Error(stacktrace.Propagatef(err, "cannot load user with api key [%s]", apiKey)) return c.Next() } diff --git a/api/pkg/middlewares/bearer_api_key_auth_middleware.go b/api/pkg/middlewares/bearer_api_key_auth_middleware.go index f52d0ba09..6125dca87 100644 --- a/api/pkg/middlewares/bearer_api_key_auth_middleware.go +++ b/api/pkg/middlewares/bearer_api_key_auth_middleware.go @@ -26,7 +26,7 @@ func BearerAPIKeyAuth(logger telemetry.Logger, tracer telemetry.Tracer, userRepo authUser, err := userRepository.LoadAuthContext(ctx, apiKey) if err != nil { - ctxLogger.Error(stacktrace.Propagate(err, "cannot load user with api key [%s] using header [%s]", apiKey, c.Get(authHeaderBearer))) + ctxLogger.Error(stacktrace.Propagatef(err, "cannot load user with api key [%s] using header [%s]", apiKey, c.Get(authHeaderBearer))) return c.Next() } diff --git a/api/pkg/middlewares/bearer_auth_middleware.go b/api/pkg/middlewares/bearer_auth_middleware.go index 6fb57a648..3391e8759 100644 --- a/api/pkg/middlewares/bearer_auth_middleware.go +++ b/api/pkg/middlewares/bearer_auth_middleware.go @@ -33,7 +33,7 @@ func BearerAuth(logger telemetry.Logger, tracer telemetry.Tracer, authClient *au token, err := authClient.VerifyIDToken(context.Background(), authToken) if err != nil { - ctxLogger.Warn(tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "invalid firebase id token [%s]", authToken))) + ctxLogger.Warn(tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "invalid firebase id token [%s]", authToken))) return c.Next() } diff --git a/api/pkg/middlewares/http_request_logger_middleware.go b/api/pkg/middlewares/http_request_logger_middleware.go index 06f5e06da..e8de42e2a 100644 --- a/api/pkg/middlewares/http_request_logger_middleware.go +++ b/api/pkg/middlewares/http_request_logger_middleware.go @@ -24,7 +24,7 @@ func HTTPRequestLogger(tracer telemetry.Tracer, logger telemetry.Logger) fiber.H statusCode := c.Response().StatusCode() span.AddEvent(fmt.Sprintf("finished handling request with traceID: [%s], statusCode: [%d]", span.SpanContext().TraceID().String(), statusCode)) if statusCode >= 300 && len(c.Request().Body()) > 0 && !slices.Contains([]int{401, 402}, statusCode) { - ctxLogger.WithString("client.version", c.Get(clientVersionHeader)).Warn(stacktrace.NewError("http.status [%d], body [%s]", statusCode, string(c.Request().Body()))) + ctxLogger.WithString("client.version", c.Get(clientVersionHeader)).Warn(stacktrace.NewErrorf("http.status [%d], body [%s]", statusCode, string(c.Request().Body()))) } return response diff --git a/api/pkg/middlewares/phone_api_key_auth_middleware.go b/api/pkg/middlewares/phone_api_key_auth_middleware.go index fb054aa4a..22ae1f5f4 100644 --- a/api/pkg/middlewares/phone_api_key_auth_middleware.go +++ b/api/pkg/middlewares/phone_api_key_auth_middleware.go @@ -26,7 +26,7 @@ func PhoneAPIKeyAuth(logger telemetry.Logger, tracer telemetry.Tracer, repositor authUser, err := repository.LoadAuthContext(ctx, apiKey) if err != nil { - ctxLogger.Error(stacktrace.Propagate(err, "cannot load user with phone api key [%s]", apiKey)) + ctxLogger.Error(stacktrace.Propagatef(err, "cannot load user with phone api key [%s]", apiKey)) return c.Next() } diff --git a/api/pkg/repositories/google_cloud_storage_attachment_repository.go b/api/pkg/repositories/google_cloud_storage_attachment_repository.go index de0f83007..3c8b9e8c2 100644 --- a/api/pkg/repositories/google_cloud_storage_attachment_repository.go +++ b/api/pkg/repositories/google_cloud_storage_attachment_repository.go @@ -43,11 +43,11 @@ func (s *GoogleCloudStorageAttachmentRepository) Upload(ctx context.Context, pat writer.ContentType = contentType if _, err := writer.Write(data); err != nil { - return s.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot write attachment to GCS path [%s]", path)) + return s.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot write attachment to GCS path [%s]", path)) } if err := writer.Close(); err != nil { - return s.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot close GCS writer for path [%s]", path)) + return s.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot close GCS writer for path [%s]", path)) } ctxLogger.Info(fmt.Sprintf("uploaded attachment to GCS path [%s/%s] with size [%d]", s.bucket, path, len(data))) @@ -62,15 +62,15 @@ func (s *GoogleCloudStorageAttachmentRepository) Download(ctx context.Context, p reader, err := s.client.Bucket(s.bucket).Object(path).NewReader(ctx) if err != nil { if errors.Is(err, storage.ErrObjectNotExist) { - return nil, s.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCode(err, ErrCodeNotFound, "cannot open GCS reader for path [%s]", path)) + return nil, s.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCodef(err, ErrCodeNotFound, "cannot open GCS reader for path [%s]", path)) } - return nil, s.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot open GCS reader for path [%s]", path)) + return nil, s.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot open GCS reader for path [%s]", path)) } defer reader.Close() data, err := io.ReadAll(reader) if err != nil { - return nil, s.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot read attachment from GCS path [%s]", path)) + return nil, s.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot read attachment from GCS path [%s]", path)) } ctxLogger.Info(fmt.Sprintf("downloaded attachment from GCS path [%s/%s] with size [%d]", s.bucket, path, len(data))) @@ -83,7 +83,7 @@ func (s *GoogleCloudStorageAttachmentRepository) Delete(ctx context.Context, pat defer span.End() if err := s.client.Bucket(s.bucket).Object(path).Delete(ctx); err != nil { - return s.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot delete GCS object at path [%s]", path)) + return s.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot delete GCS object at path [%s]", path)) } ctxLogger.Info(fmt.Sprintf("deleted attachment from GCS path [%s/%s]", s.bucket, path)) diff --git a/api/pkg/repositories/gorm_billing_usage_repository.go b/api/pkg/repositories/gorm_billing_usage_repository.go index 02509e27d..740cd0f6e 100644 --- a/api/pkg/repositories/gorm_billing_usage_repository.go +++ b/api/pkg/repositories/gorm_billing_usage_repository.go @@ -40,7 +40,7 @@ func (repository *gormBillingUsageRepository) DeleteAllForUser(ctx context.Conte defer span.End() if err := repository.db.WithContext(ctx).Where("user_id = ?", userID).Delete(&entities.BillingUsage{}).Error; err != nil { - return repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot delete all [%T] for user with ID [%s]", &entities.BillingUsage{}, userID)) + return repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot delete all [%T] for user with ID [%s]", &entities.BillingUsage{}, userID)) } return nil @@ -133,7 +133,7 @@ func (repository *gormBillingUsageRepository) GetCurrent(ctx context.Context, us }, ) if err != nil { - return &usage, stacktrace.Propagate(err, "cannot load billing usage for user [%s]", userID) + return &usage, stacktrace.Propagatef(err, "cannot load billing usage for user [%s]", userID) } return &usage, nil @@ -156,7 +156,7 @@ func (repository *gormBillingUsageRepository) GetHistory(ctx context.Context, us Find(&usages). Error if err != nil { - return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot fetch billing usage history for userID [%s] and params [%+#v]", userID, params)) + return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot fetch billing usage history for userID [%s] and params [%+#v]", userID, params)) } return usages, nil @@ -167,7 +167,7 @@ func (repository *gormBillingUsageRepository) GetHistory(ctx context.Context, us func (repository *gormBillingUsageRepository) createBillingUsageForUser(ctx context.Context, tx *gorm.DB, userID entities.UserID, timestamp time.Time, sent uint, received uint) (*entities.BillingUsage, error) { user := new(entities.User) if err := tx.WithContext(ctx).First(user, userID).Error; err != nil { - return nil, stacktrace.Propagate(err, "cannot load user [%s] to compute billing cycle", userID) + return nil, stacktrace.Propagatef(err, "cannot load user [%s] to compute billing cycle", userID) } start, end := computeBillingCycle(timestamp, user.GetBillingAnchorDay()) diff --git a/api/pkg/repositories/gorm_discord_repository.go b/api/pkg/repositories/gorm_discord_repository.go index 462ee37b1..9102b382a 100644 --- a/api/pkg/repositories/gorm_discord_repository.go +++ b/api/pkg/repositories/gorm_discord_repository.go @@ -37,7 +37,7 @@ func (repository *gormDiscordRepository) DeleteAllForUser(ctx context.Context, u defer span.End() if err := repository.db.WithContext(ctx).Where("user_id = ?", userID).Delete(&entities.Discord{}).Error; err != nil { - return repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot delete all [%T] for user with ID [%s]", &entities.Discord{}, userID)) + return repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot delete all [%T] for user with ID [%s]", &entities.Discord{}, userID)) } return nil @@ -48,7 +48,7 @@ func (repository *gormDiscordRepository) Save(ctx context.Context, Discord *enti defer span.End() if err := repository.db.WithContext(ctx).Save(Discord).Error; err != nil { - return repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot update discord integration with ID [%s]", Discord.ID)) + return repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot update discord integration with ID [%s]", Discord.ID)) } return nil @@ -67,7 +67,7 @@ func (repository *gormDiscordRepository) Index(ctx context.Context, userID entit discords := make([]*entities.Discord, 0) if err := query.Order("created_at DESC").Limit(params.Limit).Offset(params.Skip).Find(&discords).Error; err != nil { - return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot fetch discord integrations for user [%s] and params [%+#v]", userID, params)) + return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot fetch discord integrations for user [%s] and params [%+#v]", userID, params)) } return discords, nil @@ -85,7 +85,7 @@ func (repository *gormDiscordRepository) FetchHavingIncomingChannel(ctx context. Where("incoming_channel_id != ?", ""). Find(&discords).Error if err != nil { - return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot load discord integrations for user with ID [%s] having a valid [incoming_channel_id]", userID)) + return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot load discord integrations for user with ID [%s] having a valid [incoming_channel_id]", userID)) } return discords, nil @@ -98,11 +98,11 @@ func (repository *gormDiscordRepository) Load(ctx context.Context, userID entiti discord := new(entities.Discord) err := repository.db.WithContext(ctx).Where("user_id = ?", userID).Where("id = ?", discordID).First(&discord).Error if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, repository.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCode(err, ErrCodeNotFound, "discord integration with ID [%s] for user [%s] does not exist", discordID, userID)) + return nil, repository.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCodef(err, ErrCodeNotFound, "discord integration with ID [%s] for user [%s] does not exist", discordID, userID)) } if err != nil { - return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot load discord integration with ID [%s] for user [%s]", discordID, userID)) + return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot load discord integration with ID [%s] for user [%s]", discordID, userID)) } return discord, nil @@ -115,11 +115,11 @@ func (repository *gormDiscordRepository) FindByServerID(ctx context.Context, ser discord := new(entities.Discord) err := repository.db.WithContext(ctx).Where("server_id = ?", serverID).First(&discord).Error if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, repository.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCode(err, ErrCodeNotFound, "discord integration with server ID [%s] does not exist", serverID)) + return nil, repository.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCodef(err, ErrCodeNotFound, "discord integration with server ID [%s] does not exist", serverID)) } if err != nil { - return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot load discord integration with serverID [%s]", serverID)) + return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot load discord integration with serverID [%s]", serverID)) } return discord, nil @@ -134,7 +134,7 @@ func (repository *gormDiscordRepository) Delete(ctx context.Context, userID enti Where("id = ?", discordID). Delete(&entities.Discord{}).Error if err != nil { - return repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot delete discord integration with ID [%s] and userID [%s]", discordID, userID)) + return repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot delete discord integration with ID [%s] and userID [%s]", discordID, userID)) } return nil diff --git a/api/pkg/repositories/gorm_heartbeat_monitor_repository.go b/api/pkg/repositories/gorm_heartbeat_monitor_repository.go index 364e64351..50a2cefb4 100644 --- a/api/pkg/repositories/gorm_heartbeat_monitor_repository.go +++ b/api/pkg/repositories/gorm_heartbeat_monitor_repository.go @@ -40,7 +40,7 @@ func (repository *gormHeartbeatMonitorRepository) DeleteAllForUser(ctx context.C defer span.End() if err := repository.db.WithContext(ctx).Where("user_id = ?", userID).Delete(&entities.HeartbeatMonitor{}).Error; err != nil { - return repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot delete all [%T] for user with ID [%s]", &entities.HeartbeatMonitor{}, userID)) + return repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot delete all [%T] for user with ID [%s]", &entities.HeartbeatMonitor{}, userID)) } return nil } @@ -62,7 +62,7 @@ func (repository *gormHeartbeatMonitorRepository) UpdatePhoneOnline(ctx context. "updated_at": time.Now().UTC(), }).Error if err != nil { - return repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot update heartbeat monitor ID [%s] for user [%s]", monitorID, userID)) + return repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot update heartbeat monitor ID [%s] for user [%s]", monitorID, userID)) } return nil } @@ -83,7 +83,7 @@ func (repository *gormHeartbeatMonitorRepository) UpdateQueueID(ctx context.Cont "updated_at": time.Now().UTC(), }).Error if err != nil { - return repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot update heartbeat monitor ID [%s]", monitorID)) + return repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot update heartbeat monitor ID [%s]", monitorID)) } return nil } @@ -100,7 +100,7 @@ func (repository *gormHeartbeatMonitorRepository) Delete(ctx context.Context, us Where("owner = ?", owner). Delete(&entities.HeartbeatMonitor{}).Error if err != nil { - return repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot delete heartbeat monitor with owner [%s] and userID [%s]", owner, userID)) + return repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot delete heartbeat monitor with owner [%s] and userID [%s]", owner, userID)) } return nil @@ -117,7 +117,7 @@ func (repository *gormHeartbeatMonitorRepository) Index(ctx context.Context, use query := repository.db.WithContext(ctx).Where("user_id = ?", userID).Where("owner = ?", owner) heartbeats := new([]entities.Heartbeat) if err := query.Order("timestamp DESC").Limit(params.Limit).Offset(params.Skip).Find(&heartbeats).Error; err != nil { - return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot fetch heartbeats with owner [%s] and params [%+#v]", owner, params)) + return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot fetch heartbeats with owner [%s] and params [%+#v]", owner, params)) } return heartbeats, nil @@ -132,7 +132,7 @@ func (repository *gormHeartbeatMonitorRepository) Store(ctx context.Context, hea defer cancel() if err := repository.db.WithContext(ctx).Create(heartbeatMonitor).Error; err != nil { - return repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot save heartbeatMonitor monitor with ID [%s]", heartbeatMonitor.ID)) + return repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot save heartbeatMonitor monitor with ID [%s]", heartbeatMonitor.ID)) } return nil @@ -152,11 +152,11 @@ func (repository *gormHeartbeatMonitorRepository) Load(ctx context.Context, user Where("owner = ?", owner). First(&phone).Error if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, repository.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCode(err, ErrCodeNotFound, "heartbeat monitor with userID [%s] and owner [%s] does not exist", userID, owner)) + return nil, repository.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCodef(err, ErrCodeNotFound, "heartbeat monitor with userID [%s] and owner [%s] does not exist", userID, owner)) } if err != nil { - return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot load heartbeat monitor with userID [%s] and owner [%s]", userID, owner)) + return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot load heartbeat monitor with userID [%s] and owner [%s]", userID, owner)) } return phone, nil @@ -178,7 +178,7 @@ func (repository *gormHeartbeatMonitorRepository) Exists(ctx context.Context, us Where("id = ?", monitorID). Find(&exists).Error if err != nil { - return exists, repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot check if heartbeat monitor exists with userID [%s] and montior ID [%s]", userID, monitorID)) + return exists, repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot check if heartbeat monitor exists with userID [%s] and montior ID [%s]", userID, monitorID)) } return exists, nil diff --git a/api/pkg/repositories/gorm_heartbeat_repository.go b/api/pkg/repositories/gorm_heartbeat_repository.go index 544d7eed7..940718171 100644 --- a/api/pkg/repositories/gorm_heartbeat_repository.go +++ b/api/pkg/repositories/gorm_heartbeat_repository.go @@ -38,7 +38,7 @@ func (repository *gormHeartbeatRepository) DeleteAllForUser(ctx context.Context, err := repository.db.WithContext(ctx).Where("user_id = ?", userID).Delete(&entities.Heartbeat{}).Error if err != nil { - return repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot delete all [%T] for user with ID [%s]", &entities.Heartbeat{}, userID)) + return repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot delete all [%T] for user with ID [%s]", &entities.Heartbeat{}, userID)) } return nil @@ -58,11 +58,11 @@ func (repository *gormHeartbeatRepository) Last(ctx context.Context, userID enti Order("timestamp DESC"). First(&heartbeat).Error if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, repository.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCode(err, ErrCodeNotFound, "heartbeat with userID [%s] and owner [%s] does not exist", userID, owner)) + return nil, repository.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCodef(err, ErrCodeNotFound, "heartbeat with userID [%s] and owner [%s] does not exist", userID, owner)) } if err != nil { - return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot load heartbeat with userID [%s] and owner [%s]", userID, owner)) + return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot load heartbeat with userID [%s] and owner [%s]", userID, owner)) } return heartbeat, nil @@ -85,7 +85,7 @@ func (repository *gormHeartbeatRepository) Index(ctx context.Context, userID ent heartbeats := new([]entities.Heartbeat) err := query.Order("timestamp DESC").Limit(params.Limit).Offset(params.Skip).Find(&heartbeats).Error if err != nil { - return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot fetch heartbeats with owner [%s] and params [%+#v]", owner, params)) + return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot fetch heartbeats with owner [%s] and params [%+#v]", owner, params)) } return heartbeats, nil @@ -100,7 +100,7 @@ func (repository *gormHeartbeatRepository) Store(ctx context.Context, heartbeat defer cancel() if err := repository.db.WithContext(ctx).Create(heartbeat).Error; err != nil { - return repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot save heartbeat with ID [%s]", heartbeat.ID)) + return repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot save heartbeat with ID [%s]", heartbeat.ID)) } return nil diff --git a/api/pkg/repositories/gorm_integration_3cx_repository.go b/api/pkg/repositories/gorm_integration_3cx_repository.go index c63e5370b..233f316c1 100644 --- a/api/pkg/repositories/gorm_integration_3cx_repository.go +++ b/api/pkg/repositories/gorm_integration_3cx_repository.go @@ -36,7 +36,7 @@ func (repository *gormIntegration3CxRepository) DeleteAllForUser(ctx context.Con defer span.End() if err := repository.db.WithContext(ctx).Where("user_id = ?", userID).Delete(&entities.Integration3CX{}).Error; err != nil { - return repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot delete all [%T] for user with ID [%s]", &entities.Integration3CX{}, userID)) + return repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot delete all [%T] for user with ID [%s]", &entities.Integration3CX{}, userID)) } return nil @@ -50,11 +50,11 @@ func (repository *gormIntegration3CxRepository) Load(ctx context.Context, userID integration := new(entities.Integration3CX) err := repository.db.WithContext(ctx).Where("user_id = ?", userID).First(&integration).Error if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, repository.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCode(err, ErrCodeNotFound, "[3cx] integration for user [%s] does not exist", userID)) + return nil, repository.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCodef(err, ErrCodeNotFound, "[3cx] integration for user [%s] does not exist", userID)) } if err != nil { - return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot load [3cx] integration for user [%s]", userID)) + return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot load [3cx] integration for user [%s]", userID)) } return integration, nil @@ -66,7 +66,7 @@ func (repository *gormIntegration3CxRepository) Save(ctx context.Context, integr defer span.End() if err := repository.db.WithContext(ctx).Save(integration).Error; err != nil { - return repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot save [%T] with ID [%s]", integration, integration.ID)) + return repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot save [%T] with ID [%s]", integration, integration.ID)) } return nil diff --git a/api/pkg/repositories/gorm_message_repository.go b/api/pkg/repositories/gorm_message_repository.go index bb9c55be7..a4bf309f5 100644 --- a/api/pkg/repositories/gorm_message_repository.go +++ b/api/pkg/repositories/gorm_message_repository.go @@ -40,7 +40,7 @@ func (repository *gormMessageRepository) DeleteAllForUser(ctx context.Context, u defer span.End() if err := repository.db.WithContext(ctx).Where("user_id = ?", userID).Delete(&entities.Message{}).Error; err != nil { - return repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot delete all [%T] for user with ID [%s]", &entities.Message{}, userID)) + return repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot delete all [%T] for user with ID [%s]", &entities.Message{}, userID)) } return nil @@ -58,7 +58,7 @@ func (repository *gormMessageRepository) DeleteByOwnerAndContact(ctx context.Con Delete(&entities.Message{}). Error if err != nil { - return repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot delete messages between owner [%s] and contact [%s] for user with ID [%s]", owner, contact, userID)) + return repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot delete messages between owner [%s] and contact [%s] for user with ID [%s]", owner, contact, userID)) } return nil @@ -71,7 +71,7 @@ func (repository *gormMessageRepository) Delete(ctx context.Context, userID enti err := repository.db.WithContext(ctx).Where("user_id = ?", userID).Where("id = ?", messageID).Delete(&entities.Message{}).Error if err != nil { - return repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot delete message with ID [%s] for user with ID [%s]", messageID, userID)) + return repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot delete message with ID [%s] for user with ID [%s]", messageID, userID)) } return nil @@ -94,7 +94,7 @@ func (repository *gormMessageRepository) Index(ctx context.Context, userID entit messages := new([]entities.Message) if err := query.Order("order_timestamp DESC").Limit(params.Limit).Offset(params.Skip).Find(&messages).Error; err != nil { - return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot fetch messges with owner [%s] and contact [%s] and params [%+#v]", owner, contact, params)) + return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot fetch messges with owner [%s] and contact [%s] and params [%+#v]", owner, contact, params)) } return messages, nil @@ -114,11 +114,11 @@ func (repository *gormMessageRepository) LastMessage(ctx context.Context, userID err := query.Order("order_timestamp DESC").First(&message).Error if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, repository.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCode(err, ErrCodeNotFound, "cannot get last message for [%s] with owner [%s] and contact [%s]", userID, owner, contact)) + return nil, repository.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCodef(err, ErrCodeNotFound, "cannot get last message for [%s] with owner [%s] and contact [%s]", userID, owner, contact)) } if err != nil { - return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot get last message for [%s] with owner [%s] and contact [%s]", userID, owner, contact)) + return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot get last message for [%s] with owner [%s] and contact [%s]", userID, owner, contact)) } return message, nil @@ -163,7 +163,7 @@ func (repository *gormMessageRepository) Search(ctx context.Context, userID enti Find(&messages). Error if err != nil { - return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot search messages with for user [%s] params [%+#v]", userID, params)) + return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot search messages with for user [%s] params [%+#v]", userID, params)) } return messages, nil @@ -193,7 +193,7 @@ func (repository *gormMessageRepository) GetBulkMessages(ctx context.Context, us LIMIT ? `, userID, limit).Scan(&orders).Error if err != nil { - return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot fetch bulk message orders for user [%s]", userID)) + return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot fetch bulk message orders for user [%s]", userID)) } return orders, nil @@ -205,7 +205,7 @@ func (repository *gormMessageRepository) Store(ctx context.Context, message *ent defer span.End() if err := repository.db.WithContext(ctx).Create(message).Error; err != nil { - return repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot save message with ID [%s]", message.ID)) + return repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot save message with ID [%s]", message.ID)) } return nil @@ -219,11 +219,11 @@ func (repository *gormMessageRepository) Load(ctx context.Context, userID entiti message := new(entities.Message) err := repository.db.WithContext(ctx).Where("user_id = ?", userID).Where("id = ?", messageID).First(message).Error if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, repository.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCode(err, ErrCodeNotFound, "message with ID [%s] and userID [%s] does not exist", messageID, userID)) + return nil, repository.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCodef(err, ErrCodeNotFound, "message with ID [%s] and userID [%s] does not exist", messageID, userID)) } if err != nil { - return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot load message with ID [%s]", messageID)) + return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot load message with ID [%s]", messageID)) } return message, nil @@ -235,7 +235,7 @@ func (repository *gormMessageRepository) Update(ctx context.Context, message *en defer span.End() if err := repository.db.WithContext(ctx).Save(message).Error; err != nil { - return repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot update message with ID [%s]", message.ID)) + return repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot update message with ID [%s]", message.ID)) } return nil @@ -264,15 +264,15 @@ func (repository *gormMessageRepository) GetOutstanding(ctx context.Context, use }, ) if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, repository.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCode(err, ErrCodeNotFound, "outstanding message with ID [%s] and userID [%s] does not exist", messageID, userID)) + return nil, repository.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCodef(err, ErrCodeNotFound, "outstanding message with ID [%s] and userID [%s] does not exist", messageID, userID)) } if err != nil { - return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot fetch outstanding message with userID [%s] and messageID [%s]", userID, messageID)) + return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot fetch outstanding message with userID [%s] and messageID [%s]", userID, messageID)) } if message == nil || message.ID == uuid.Nil { - return nil, repository.tracer.WrapErrorSpan(span, stacktrace.NewErrorWithCode(ErrCodeNotFound, "outstanding message with ID [%s] and userID [%s] does not exist", messageID, userID)) + return nil, repository.tracer.WrapErrorSpan(span, stacktrace.NewErrorWithCodef(ErrCodeNotFound, "outstanding message with ID [%s] and userID [%s] does not exist", messageID, userID)) } return message, nil diff --git a/api/pkg/repositories/gorm_message_send_schedule_repository.go b/api/pkg/repositories/gorm_message_send_schedule_repository.go index 49538ce05..a3d0be6c1 100644 --- a/api/pkg/repositories/gorm_message_send_schedule_repository.go +++ b/api/pkg/repositories/gorm_message_send_schedule_repository.go @@ -41,7 +41,7 @@ func (r *gormMessageSendScheduleRepository) Store( defer span.End() if err := r.db.WithContext(ctx).Create(schedule).Error; err != nil { - return r.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot store send schedule [%s]", schedule.ID)) + return r.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot store send schedule [%s]", schedule.ID)) } return nil @@ -56,7 +56,7 @@ func (r *gormMessageSendScheduleRepository) Update( defer span.End() if err := r.db.WithContext(ctx).Save(schedule).Error; err != nil { - return r.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot update send schedule [%s]", schedule.ID)) + return r.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot update send schedule [%s]", schedule.ID)) } return nil @@ -79,11 +79,11 @@ func (r *gormMessageSendScheduleRepository) Load( if errors.Is(err, gorm.ErrRecordNotFound) { return nil, r.tracer.WrapErrorSpan( span, - stacktrace.PropagateWithCode(err, ErrCodeNotFound, "send schedule [%s] not found for user with ID [%s]", scheduleID, userID), + stacktrace.PropagateWithCodef(err, ErrCodeNotFound, "send schedule [%s] not found for user with ID [%s]", scheduleID, userID), ) } if err != nil { - return nil, r.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot load send schedule [%s]", scheduleID)) + return nil, r.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot load send schedule [%s]", scheduleID)) } return item, nil @@ -103,7 +103,7 @@ func (r *gormMessageSendScheduleRepository) Index( Order("created_at DESC"). Find(&items).Error if err != nil { - return nil, r.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot index send schedules for user [%s]", userID)) + return nil, r.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot index send schedules for user [%s]", userID)) } return items, nil @@ -123,7 +123,7 @@ func (r *gormMessageSendScheduleRepository) Delete( Where("id = ?", scheduleID). Delete(&entities.MessageSendSchedule{}).Error if err != nil { - return r.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot delete send schedule [%s]", scheduleID)) + return r.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot delete send schedule [%s]", scheduleID)) } return nil @@ -141,7 +141,7 @@ func (r *gormMessageSendScheduleRepository) DeleteAllForUser( Where("user_id = ?", userID). Delete(&entities.MessageSendSchedule{}).Error if err != nil { - return r.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot delete send schedules for user [%s]", userID)) + return r.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot delete send schedules for user [%s]", userID)) } return nil @@ -161,7 +161,7 @@ func (r *gormMessageSendScheduleRepository) CountByUser( Where("user_id = ?", userID). Count(&count).Error if err != nil { - return 0, r.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot count send schedules for user [%s]", userID)) + return 0, r.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot count send schedules for user [%s]", userID)) } return int(count), nil diff --git a/api/pkg/repositories/gorm_message_thread_repository.go b/api/pkg/repositories/gorm_message_thread_repository.go index 88506f485..092418040 100644 --- a/api/pkg/repositories/gorm_message_thread_repository.go +++ b/api/pkg/repositories/gorm_message_thread_repository.go @@ -82,7 +82,7 @@ func (repository *gormMessageThreadRepository) DeleteAllForUser(ctx context.Cont defer span.End() if err := repository.db.WithContext(ctx).Where("user_id = ?", userID).Delete(&entities.MessageThread{}).Error; err != nil { - return repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot delete all [%T] for user with ID [%s]", &entities.MessageThread{}, userID)) + return repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot delete all [%T] for user with ID [%s]", &entities.MessageThread{}, userID)) } return nil @@ -95,7 +95,7 @@ func (repository *gormMessageThreadRepository) Delete(ctx context.Context, userI err := repository.db.WithContext(ctx).Where("user_id = ?", userID).Where("id = ?", messageThreadID).Delete(&entities.MessageThread{}).Error if err != nil { - return repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot delete message thread with ID [%s] for user with ID [%s]", messageThreadID, userID)) + return repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot delete message thread with ID [%s] for user with ID [%s]", messageThreadID, userID)) } return nil @@ -112,7 +112,7 @@ func (repository *gormMessageThreadRepository) UpdateAfterDeletedMessage(ctx con Where("id = ?", params.MessageThreadID). Updates(messageThreadDeletedUpdates(params)) if result.Error != nil { - return repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(result.Error, "cannot update deleted-message metadata for thread [%s]", params.MessageThreadID)) + return repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(result.Error, "cannot update deleted-message metadata for thread [%s]", params.MessageThreadID)) } return nil @@ -141,7 +141,7 @@ func (repository *gormMessageThreadRepository) Store(ctx context.Context, thread Error }) if err != nil { - return repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot save message thread with ID [%s]", thread.ID)) + return repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot save message thread with ID [%s]", thread.ID)) } return nil @@ -158,10 +158,10 @@ func (repository *gormMessageThreadRepository) UpdateActivity(ctx context.Contex Where("id = ?", params.MessageThreadID). Updates(messageThreadActivityUpdates(params)) if result.Error != nil { - return repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(result.Error, "cannot update message activity for thread [%s]", params.MessageThreadID)) + return repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(result.Error, "cannot update message activity for thread [%s]", params.MessageThreadID)) } if result.RowsAffected == 0 { - return repository.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCode(gorm.ErrRecordNotFound, ErrCodeNotFound, "thread with id [%s] not found", params.MessageThreadID)) + return repository.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCodef(gorm.ErrRecordNotFound, ErrCodeNotFound, "thread with id [%s] not found", params.MessageThreadID)) } return nil @@ -185,10 +185,10 @@ func (repository *gormMessageThreadRepository) UpdateStatus( Where("id = ?", messageThreadID). Updates(messageThreadStatusUpdates(params)) if result.Error != nil { - return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(result.Error, "cannot update status for thread [%s] and user [%s]", messageThreadID, userID)) + return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(result.Error, "cannot update status for thread [%s] and user [%s]", messageThreadID, userID)) } if result.RowsAffected == 0 { - return nil, repository.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCode(gorm.ErrRecordNotFound, ErrCodeNotFound, "thread with id [%s] not found for user with ID [%s]", messageThreadID, userID)) + return nil, repository.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCodef(gorm.ErrRecordNotFound, ErrCodeNotFound, "thread with id [%s] not found for user with ID [%s]", messageThreadID, userID)) } return thread, nil @@ -209,11 +209,11 @@ func (repository *gormMessageThreadRepository) LoadByOwnerContact(ctx context.Co First(thread). Error if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, repository.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCode(err, ErrCodeNotFound, "thread with owner [%s] and contact [%s] does not exist", owner, contact)) + return nil, repository.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCodef(err, ErrCodeNotFound, "thread with owner [%s] and contact [%s] does not exist", owner, contact)) } if err != nil { - return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot load thread with owner [%s] and contact [%s]", owner, contact)) + return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot load thread with owner [%s] and contact [%s]", owner, contact)) } return thread, nil @@ -233,11 +233,11 @@ func (repository *gormMessageThreadRepository) Load(ctx context.Context, userID First(thread). Error if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, repository.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCode(err, ErrCodeNotFound, "thread with id [%s] not found", ID)) + return nil, repository.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCodef(err, ErrCodeNotFound, "thread with id [%s] not found", ID)) } if err != nil { - return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "thread with id [%s]", ID)) + return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "thread with id [%s]", ID)) } return thread, nil @@ -270,7 +270,7 @@ func (repository *gormMessageThreadRepository) Index(ctx context.Context, userID threads := new([]entities.MessageThread) if err := query.Order("order_timestamp DESC").Limit(params.Limit).Offset(params.Skip).Find(&threads).Error; err != nil { - return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot fetch message threads with owner [%s] and params [%+#v]", owner, params)) + return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot fetch message threads with owner [%s] and params [%+#v]", owner, params)) } return threads, nil diff --git a/api/pkg/repositories/gorm_phone_api_key_repository.go b/api/pkg/repositories/gorm_phone_api_key_repository.go index 51502d1a7..aa64436c6 100644 --- a/api/pkg/repositories/gorm_phone_api_key_repository.go +++ b/api/pkg/repositories/gorm_phone_api_key_repository.go @@ -53,7 +53,7 @@ WHERE user_id = ? AND array_position(phone_ids, ?) IS NOT NULL; Exec(query, phoneID, phoneNumber, userID, phoneID). Error if err != nil { - return repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot remove phone with ID [%s] and number [%s] for user with ID [%s] ", phoneID, phoneNumber, userID)) + return repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot remove phone with ID [%s] and number [%s] for user with ID [%s] ", phoneID, phoneNumber, userID)) } repository.cache.Clear() @@ -71,7 +71,7 @@ func (repository *gormPhoneAPIKeyRepository) CountByUser(ctx context.Context, us Where("user_id = ?", userID). Count(&count).Error if err != nil { - return 0, repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot count phone API keys for user [%s]", userID)) + return 0, repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot count phone API keys for user [%s]", userID)) } return int(count), nil @@ -85,11 +85,11 @@ func (repository *gormPhoneAPIKeyRepository) Load(ctx context.Context, userID en phoneAPIKey := new(entities.PhoneAPIKey) err := repository.db.WithContext(ctx).Where("user_id = ?", userID).Where("id = ?", phoneAPIKeyID).First(&phoneAPIKey).Error if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, repository.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCode(err, ErrCodeNotFound, "[%T] with ID [%s] for user with ID [%s] does not exist", phoneAPIKey, phoneAPIKeyID, userID)) + return nil, repository.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCodef(err, ErrCodeNotFound, "[%T] with ID [%s] for user with ID [%s] does not exist", phoneAPIKey, phoneAPIKeyID, userID)) } if err != nil { - return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot load [%T] with ID [%s] for user with ID [%s]", phoneAPIKey, phoneAPIKeyID, userID)) + return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot load [%T] with ID [%s] for user with ID [%s]", phoneAPIKey, phoneAPIKeyID, userID)) } return phoneAPIKey, nil @@ -100,7 +100,7 @@ func (repository *gormPhoneAPIKeyRepository) Create(ctx context.Context, phoneAP defer span.End() if err := repository.db.WithContext(ctx).Create(phoneAPIKey).Error; err != nil { - return repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot save phone API key with ID [%s] for user with ID [%s]", phoneAPIKey.ID, phoneAPIKey.UserID)) + return repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot save phone API key with ID [%s] for user with ID [%s]", phoneAPIKey.ID, phoneAPIKey.UserID)) } return nil @@ -117,11 +117,11 @@ func (repository *gormPhoneAPIKeyRepository) LoadAuthContext(ctx context.Context phoneAPIKey := new(entities.PhoneAPIKey) err := repository.db.WithContext(ctx).Where("api_key = ?", apiKey).First(phoneAPIKey).Error if errors.Is(err, gorm.ErrRecordNotFound) { - return entities.AuthContext{}, repository.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCode(err, ErrCodeNotFound, "phone api key [%s] does not exist", apiKey)) + return entities.AuthContext{}, repository.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCodef(err, ErrCodeNotFound, "phone api key [%s] does not exist", apiKey)) } if err != nil { - return entities.AuthContext{}, repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot load phone api key [%s]", apiKey)) + return entities.AuthContext{}, repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot load phone api key [%s]", apiKey)) } authUser := entities.AuthContext{ @@ -132,7 +132,7 @@ func (repository *gormPhoneAPIKeyRepository) LoadAuthContext(ctx context.Context } if result := repository.cache.SetWithTTL(apiKey, authUser, 1, 15*time.Second); !result { - ctxLogger.Error(repository.tracer.WrapErrorSpan(span, stacktrace.NewError("cannot cache [%T] with ID [%s] and result [%t]", authUser, phoneAPIKey.ID, result))) + ctxLogger.Error(repository.tracer.WrapErrorSpan(span, stacktrace.NewErrorf("cannot cache [%T] with ID [%s] and result [%t]", authUser, phoneAPIKey.ID, result))) } return authUser, nil @@ -150,7 +150,7 @@ func (repository *gormPhoneAPIKeyRepository) Index(ctx context.Context, userID e phoneAPIKeys := new([]*entities.PhoneAPIKey) if err := query.Order("created_at DESC").Limit(params.Limit).Offset(params.Skip).Find(phoneAPIKeys).Error; err != nil { - return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot fetch phone API Keys with userID [%s] and params [%+#v]", userID, params)) + return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot fetch phone API Keys with userID [%s] and params [%+#v]", userID, params)) } return *phoneAPIKeys, nil @@ -162,7 +162,7 @@ func (repository *gormPhoneAPIKeyRepository) Delete(ctx context.Context, phoneAP err := repository.db.WithContext(ctx).Delete(phoneAPIKey).Error if err != nil { - return repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot delete phone API key with ID [%s] and userID [%s]", phoneAPIKey.ID, phoneAPIKey.UserID)) + return repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot delete phone API key with ID [%s] and userID [%s]", phoneAPIKey.ID, phoneAPIKey.UserID)) } repository.cache.Del(phoneAPIKey.APIKey) @@ -184,7 +184,7 @@ WHERE user_id = ?; Exec(query, phone.ID, phone.PhoneNumber, phone.UserID). Error if err != nil { - return repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot remove phone with ID [%s] from API Key with ID [%s] for user with ID [%s]", phone.ID, phoneAPIKey.ID, phone.UserID)) + return repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot remove phone with ID [%s] from API Key with ID [%s] for user with ID [%s]", phone.ID, phoneAPIKey.ID, phone.UserID)) } query = ` @@ -197,13 +197,13 @@ WHERE array_position(phone_ids, ?) IS NULL AND id = ?; Exec(query, phone.ID, phone.PhoneNumber, phone.ID, phoneAPIKey.ID). Error if err != nil { - return repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot add [%T] with ID [%s] from API Key with ID [%s] for user with ID [%s]", phone, phone.ID, phoneAPIKey.ID, phone.UserID)) + return repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot add [%T] with ID [%s] from API Key with ID [%s] for user with ID [%s]", phone, phone.ID, phoneAPIKey.ID, phone.UserID)) } return nil }) if err != nil { - return repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot add [%T] with ID [%s] from API Key with ID [%s] for user with ID [%s]", phone, phone.ID, phoneAPIKey.ID, phone.UserID)) + return repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot add [%T] with ID [%s] from API Key with ID [%s] for user with ID [%s]", phone, phone.ID, phoneAPIKey.ID, phone.UserID)) } repository.cache.Clear() return nil @@ -223,7 +223,7 @@ WHERE id = ?; Exec(query, phone.ID, phone.PhoneNumber, phoneAPIKey.ID). Error if err != nil { - return repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot remove phone with ID [%s] from phone API key with ID [%s]", phone.ID, phoneAPIKey.ID)) + return repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot remove phone with ID [%s] from phone API key with ID [%s]", phone.ID, phoneAPIKey.ID)) } return nil @@ -234,7 +234,7 @@ func (repository *gormPhoneAPIKeyRepository) DeleteAllForUser(ctx context.Contex defer span.End() if err := repository.db.WithContext(ctx).Where("user_id = ?", userID).Delete(&entities.PhoneAPIKey{}).Error; err != nil { - return repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot delete all [%T] for user with ID [%s]", &entities.PhoneAPIKey{}, userID)) + return repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot delete all [%T] for user with ID [%s]", &entities.PhoneAPIKey{}, userID)) } return nil diff --git a/api/pkg/repositories/gorm_phone_notification_repository.go b/api/pkg/repositories/gorm_phone_notification_repository.go index 1301ecc02..c83aaf380 100644 --- a/api/pkg/repositories/gorm_phone_notification_repository.go +++ b/api/pkg/repositories/gorm_phone_notification_repository.go @@ -48,7 +48,7 @@ func (repository *gormPhoneNotificationRepository) DeleteAllForUser( Delete(&entities.PhoneNotification{}).Error; err != nil { return repository.tracer.WrapErrorSpan( span, - stacktrace.Propagate( + stacktrace.Propagatef( err, "cannot delete all [%T] for user with ID [%s]", &entities.PhoneNotification{}, @@ -71,7 +71,7 @@ func (repository *gormPhoneNotificationRepository) DeleteByMessageID(ctx context if err != nil { return repository.tracer.WrapErrorSpan( span, - stacktrace.Propagate(err, "cannot delete [%T] for user [%s] and message with ID [%s]", &entities.PhoneNotification{}, userID, messageID), + stacktrace.Propagatef(err, "cannot delete [%T] for user [%s] and message with ID [%s]", &entities.PhoneNotification{}, userID, messageID), ) } @@ -91,7 +91,7 @@ func (repository *gormPhoneNotificationRepository) UpdateStatus(ctx context.Cont if err != nil { return repository.tracer.WrapErrorSpan( span, - stacktrace.Propagate( + stacktrace.Propagatef( err, "cannot update notification [%s] with status [%s]", notificationID, @@ -132,7 +132,7 @@ func (repository *gormPhoneNotificationRepository) Schedule( First(lastNotification). Error if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { - return stacktrace.Propagate( + return stacktrace.Propagatef( err, "cannot fetch last notification with phone ID [%s]", notification.PhoneID, @@ -151,7 +151,7 @@ func (repository *gormPhoneNotificationRepository) Schedule( } if err = tx.WithContext(ctx).Create(notification).Error; err != nil { - return stacktrace.Propagate( + return stacktrace.Propagatef( err, "cannot create new notification with id [%s] and schedule [%s]", notification.ID, @@ -164,7 +164,7 @@ func (repository *gormPhoneNotificationRepository) Schedule( if err != nil { return repository.tracer.WrapErrorSpan( span, - stacktrace.Propagate( + stacktrace.Propagatef( err, "cannot schedule phone notification with ID [%s]", notification.ID, @@ -207,7 +207,7 @@ func (repository *gormPhoneNotificationRepository) insert( if err := repository.db.WithContext(ctx).Create(notification).Error; err != nil { return repository.tracer.WrapErrorSpan( span, - stacktrace.Propagate( + stacktrace.Propagatef( err, "cannot store notification with id [%s]", notification.ID, @@ -235,7 +235,7 @@ func (repository *gormPhoneNotificationRepository) ScheduleExact( Count(&count).Error; err != nil { return repository.tracer.WrapErrorSpan( span, - stacktrace.Propagate(err, "cannot check for existing notification for message [%s]", notification.MessageID), + stacktrace.Propagatef(err, "cannot check for existing notification for message [%s]", notification.MessageID), ) } @@ -246,7 +246,7 @@ func (repository *gormPhoneNotificationRepository) ScheduleExact( if err := repository.db.WithContext(ctx).Create(notification).Error; err != nil { return repository.tracer.WrapErrorSpan( span, - stacktrace.Propagate(err, "cannot create exact-time notification with id [%s]", notification.ID), + stacktrace.Propagatef(err, "cannot create exact-time notification with id [%s]", notification.ID), ) } diff --git a/api/pkg/repositories/gorm_phone_repository.go b/api/pkg/repositories/gorm_phone_repository.go index 98c2f3fb0..bf6d69e43 100644 --- a/api/pkg/repositories/gorm_phone_repository.go +++ b/api/pkg/repositories/gorm_phone_repository.go @@ -42,7 +42,7 @@ func (repository *gormPhoneRepository) DeleteAllForUser(ctx context.Context, use defer span.End() if err := repository.db.WithContext(ctx).Where("user_id = ?", userID).Delete(&entities.Phone{}).Error; err != nil { - return repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot delete all [%T] for user with ID [%s]", &entities.Phone{}, userID)) + return repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot delete all [%T] for user with ID [%s]", &entities.Phone{}, userID)) } repository.cache.Clear() @@ -60,7 +60,7 @@ func (repository *gormPhoneRepository) NullifyScheduleID(ctx context.Context, us Where("message_send_schedule_id = ?", scheduleID). Update("message_send_schedule_id", nil).Error if err != nil { - return repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot nullify message_send_schedule_id [%s] for user [%s]", scheduleID, userID)) + return repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot nullify message_send_schedule_id [%s] for user [%s]", scheduleID, userID)) } repository.cache.Clear() @@ -78,11 +78,11 @@ func (repository *gormPhoneRepository) LoadByID(ctx context.Context, userID enti Where("id = ?", phoneID). First(&phone).Error if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, repository.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCode(err, ErrCodeNotFound, "phone with ID [%s] does not exist", phoneID)) + return nil, repository.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCodef(err, ErrCodeNotFound, "phone with ID [%s] does not exist", phoneID)) } if err != nil { - return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot load phone with ID [%s]", phoneID)) + return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot load phone with ID [%s]", phoneID)) } return phone, nil @@ -98,7 +98,7 @@ func (repository *gormPhoneRepository) Delete(ctx context.Context, userID entiti Where("id = ?", phoneID). Delete(&entities.Phone{}).Error if err != nil { - return repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot delete phone with ID [%s] and userID [%s]", phoneID, userID)) + return repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot delete phone with ID [%s] and userID [%s]", phoneID, userID)) } repository.cache.Clear() @@ -115,14 +115,14 @@ func (repository *gormPhoneRepository) Save(ctx context.Context, phone *entities ctxLogger.Info(fmt.Sprintf("phone with user [%s] and number[%s] already exists", phone.UserID, phone.PhoneNumber)) loadedPhone, err := repository.Load(ctx, phone.UserID, phone.PhoneNumber) if err != nil { - return repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot load phone for user [%s] and number [%s]", phone.UserID, phone.PhoneNumber)) + return repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot load phone for user [%s] and number [%s]", phone.UserID, phone.PhoneNumber)) } *phone = *loadedPhone return nil } if err != nil { - return repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot save phone with ID [%s]", phone.ID)) + return repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot save phone with ID [%s]", phone.ID)) } repository.cache.Del(repository.getCacheKey(phone.UserID, phone.PhoneNumber)) @@ -141,15 +141,15 @@ func (repository *gormPhoneRepository) Load(ctx context.Context, userID entities phone := new(entities.Phone) err := repository.db.WithContext(ctx).Where("user_id = ?", userID).Where("phone_number = ?", phoneNumber).First(phone).Error if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, repository.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCode(err, ErrCodeNotFound, "phone with userID [%s] and phoneNumber [%s] does not exist", userID, phoneNumber)) + return nil, repository.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCodef(err, ErrCodeNotFound, "phone with userID [%s] and phoneNumber [%s] does not exist", userID, phoneNumber)) } if err != nil { - return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot load phone phone with userID [%s] and phoneNumber [%s]", userID, phoneNumber)) + return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot load phone phone with userID [%s] and phoneNumber [%s]", userID, phoneNumber)) } if result := repository.cache.SetWithTTL(repository.getCacheKey(userID, phoneNumber), phone, 1, 30*time.Minute); !result { - ctxLogger.Error(repository.tracer.WrapErrorSpan(span, stacktrace.NewError("cannot cache [%T] with ID [%s] and result [%t]", phone, phone.ID, result))) + ctxLogger.Error(repository.tracer.WrapErrorSpan(span, stacktrace.NewErrorf("cannot cache [%T] with ID [%s] and result [%t]", phone, phone.ID, result))) } return phone, nil @@ -167,7 +167,7 @@ func (repository *gormPhoneRepository) Index(ctx context.Context, userID entitie phones := new([]entities.Phone) if err := query.Order("created_at DESC").Limit(params.Limit).Offset(params.Skip).Find(&phones).Error; err != nil { - return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot fetch phones with userID [%s] and params [%+#v]", userID, params)) + return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot fetch phones with userID [%s] and params [%+#v]", userID, params)) } return phones, nil diff --git a/api/pkg/repositories/gorm_user_repository.go b/api/pkg/repositories/gorm_user_repository.go index da46e5e73..f7c74f318 100644 --- a/api/pkg/repositories/gorm_user_repository.go +++ b/api/pkg/repositories/gorm_user_repository.go @@ -48,7 +48,7 @@ func (repository *gormUserRepository) Delete(ctx context.Context, user *entities defer span.End() if err := repository.db.WithContext(ctx).Delete(user).Error; err != nil { - return repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot delete user with ID [%s]", user.ID)) + return repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot delete user with ID [%s]", user.ID)) } return nil @@ -60,7 +60,7 @@ func (repository *gormUserRepository) RotateAPIKey(ctx context.Context, userID e apiKey, err := repository.generateAPIKey(64) if err != nil { - return nil, stacktrace.Propagate(err, "cannot generate apiKey for user [%s]", userID) + return nil, stacktrace.Propagatef(err, "cannot generate apiKey for user [%s]", userID) } user := new(entities.User) @@ -79,7 +79,7 @@ func (repository *gormUserRepository) RotateAPIKey(ctx context.Context, userID e }, ) if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, repository.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCode(err, ErrCodeNotFound, "user with ID [%s] does not exist", userID)) + return nil, repository.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCodef(err, ErrCodeNotFound, "user with ID [%s] does not exist", userID)) } if err == nil && oldAPIKey != "" { @@ -102,11 +102,11 @@ func (repository *gormUserRepository) LoadBySubscriptionID(ctx context.Context, First(user). Error if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, repository.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCode(err, ErrCodeNotFound, "user with subscriptionID [%s] does not exist", subscriptionID)) + return nil, repository.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCodef(err, ErrCodeNotFound, "user with subscriptionID [%s] does not exist", subscriptionID)) } if err != nil { - return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot load user with subscription ID [%s]", subscriptionID)) + return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot load user with subscription ID [%s]", subscriptionID)) } return user, nil @@ -122,11 +122,11 @@ func (repository *gormUserRepository) LoadByEmail(ctx context.Context, email str First(user). Error if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, repository.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCode(err, ErrCodeNotFound, "user with email [%s] does not exist", email)) + return nil, repository.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCodef(err, ErrCodeNotFound, "user with email [%s] does not exist", email)) } if err != nil { - return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot load user with email ID [%s]", email)) + return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot load user with email ID [%s]", email)) } return user, nil @@ -137,7 +137,7 @@ func (repository *gormUserRepository) Store(ctx context.Context, user *entities. defer span.End() if err := repository.db.WithContext(ctx).Create(user).Error; err != nil { - return repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot save user with ID [%s]", user.ID)) + return repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot save user with ID [%s]", user.ID)) } return nil @@ -148,7 +148,7 @@ func (repository *gormUserRepository) Update(ctx context.Context, user *entities defer span.End() if err := repository.db.WithContext(ctx).Save(user).Error; err != nil { - return repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot update user with ID [%s]", user.ID)) + return repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot update user with ID [%s]", user.ID)) } return nil @@ -160,7 +160,7 @@ func (repository *gormUserRepository) LoadAuthContext(ctx context.Context, apiKe if authUser, found := repository.cache.Get(apiKey); found { if authUser.IsNoop() { - return authUser, repository.tracer.WrapErrorSpan(span, stacktrace.NewError("user with api key [%s] does not exist", apiKey)) + return authUser, repository.tracer.WrapErrorSpan(span, stacktrace.NewErrorf("user with api key [%s] does not exist", apiKey)) } return authUser, nil } @@ -170,11 +170,11 @@ func (repository *gormUserRepository) LoadAuthContext(ctx context.Context, apiKe if errors.Is(err, gorm.ErrRecordNotFound) { repository.cache.SetWithTTL(apiKey, entities.AuthContext{}, 1, 2*time.Hour) - return entities.AuthContext{}, repository.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCode(err, ErrCodeNotFound, "user with api key [%s] does not exist", apiKey)) + return entities.AuthContext{}, repository.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCodef(err, ErrCodeNotFound, "user with api key [%s] does not exist", apiKey)) } if err != nil { - return entities.AuthContext{}, repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot load user with api key [%s]", apiKey)) + return entities.AuthContext{}, repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot load user with api key [%s]", apiKey)) } authUser := entities.AuthContext{ @@ -183,7 +183,7 @@ func (repository *gormUserRepository) LoadAuthContext(ctx context.Context, apiKe } if result := repository.cache.SetWithTTL(apiKey, authUser, 1, 2*time.Hour); !result { - ctxLogger.Error(repository.tracer.WrapErrorSpan(span, stacktrace.NewError("cannot cache [%T] with ID [%s] and result [%t]", authUser, user.ID, result))) + ctxLogger.Error(repository.tracer.WrapErrorSpan(span, stacktrace.NewErrorf("cannot cache [%T] with ID [%s] and result [%t]", authUser, user.ID, result))) } return authUser, nil @@ -196,11 +196,11 @@ func (repository *gormUserRepository) Load(ctx context.Context, userID entities. user := new(entities.User) err := repository.db.WithContext(ctx).First(user, userID).Error if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, repository.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCode(err, ErrCodeNotFound, "user with ID [%s] does not exist", userID)) + return nil, repository.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCodef(err, ErrCodeNotFound, "user with ID [%s] does not exist", userID)) } if err != nil { - return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot load user with ID [%s]", userID)) + return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot load user with ID [%s]", userID)) } return user, nil @@ -217,7 +217,7 @@ func (repository *gormUserRepository) LoadOrStore(ctx context.Context, authUser apiKey, err := repository.generateAPIKey(64) if err != nil { - return nil, false, stacktrace.Propagate(err, "cannot generate apiKey for user [%s]", authUser.ID) + return nil, false, stacktrace.Propagatef(err, "cannot generate apiKey for user [%s]", authUser.ID) } user = &entities.User{ @@ -241,7 +241,7 @@ func (repository *gormUserRepository) LoadOrStore(ctx context.Context, authUser return result.Error }) if err != nil { - return user, isNew, repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot create user from auth user [%+#v]", authUser)) + return user, isNew, repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot create user from auth user [%+#v]", authUser)) } return user, isNew, nil @@ -255,7 +255,7 @@ func (repository *gormUserRepository) generateRandomBytes(n int) ([]byte, error) b := make([]byte, n) // Note that err == nil only if we read len(b) bytes. if _, err := rand.Read(b); err != nil { - return nil, stacktrace.Propagate(err, "cannot generate [%d] random bytes", n) + return nil, stacktrace.Propagatef(err, "cannot generate [%d] random bytes", n) } return b, nil @@ -268,5 +268,5 @@ func (repository *gormUserRepository) generateRandomBytes(n int) ([]byte, error) // case the caller should not continue. func (repository *gormUserRepository) generateAPIKey(n int) (string, error) { b, err := repository.generateRandomBytes(n) - return base64.URLEncoding.EncodeToString(b)[0:n], stacktrace.Propagate(err, "cannot generate random bytes") + return base64.URLEncoding.EncodeToString(b)[0:n], stacktrace.Propagatef(err, "cannot generate random bytes") } diff --git a/api/pkg/repositories/gorm_webhook_repository.go b/api/pkg/repositories/gorm_webhook_repository.go index c56bd3a73..5cf23b077 100644 --- a/api/pkg/repositories/gorm_webhook_repository.go +++ b/api/pkg/repositories/gorm_webhook_repository.go @@ -37,7 +37,7 @@ func (repository *gormWebhookRepository) DeleteAllForUser(ctx context.Context, u defer span.End() if err := repository.db.WithContext(ctx).Where("user_id = ?", userID).Delete(&entities.Webhook{}).Error; err != nil { - return repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot delete all [%T] for user with ID [%s]", &entities.Webhook{}, userID)) + return repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot delete all [%T] for user with ID [%s]", &entities.Webhook{}, userID)) } return nil @@ -48,7 +48,7 @@ func (repository *gormWebhookRepository) Save(ctx context.Context, webhook *enti defer span.End() if err := repository.db.WithContext(ctx).Save(webhook).Error; err != nil { - return repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot update webhook with ID [%s]", webhook.ID)) + return repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot update webhook with ID [%s]", webhook.ID)) } return nil @@ -67,7 +67,7 @@ func (repository *gormWebhookRepository) Index(ctx context.Context, userID entit webhooks := make([]*entities.Webhook, 0) if err := query.Order("created_at DESC").Limit(params.Limit).Offset(params.Skip).Find(&webhooks).Error; err != nil { - return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot fetch webhooks for user [%s] and params [%+#v]", userID, params)) + return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot fetch webhooks for user [%s] and params [%+#v]", userID, params)) } return webhooks, nil @@ -83,7 +83,7 @@ func (repository *gormWebhookRepository) LoadByEvent(ctx context.Context, userID Scan(&webhooks). Error if err != nil { - return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot load webhooks for user with ID [%s] and event [%s]", userID, event)) + return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot load webhooks for user with ID [%s] and event [%s]", userID, event)) } return webhooks, nil @@ -96,11 +96,11 @@ func (repository *gormWebhookRepository) Load(ctx context.Context, userID entiti webhook := new(entities.Webhook) err := repository.db.WithContext(ctx).Where("user_id = ?", userID).Where("id = ?", webhookID).First(&webhook).Error if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, repository.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCode(err, ErrCodeNotFound, "webhook with ID [%s] for user [%s] does not exist", webhookID, userID)) + return nil, repository.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCodef(err, ErrCodeNotFound, "webhook with ID [%s] for user [%s] does not exist", webhookID, userID)) } if err != nil { - return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot load webhook with ID [%s] for user [%s]", webhookID, userID)) + return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot load webhook with ID [%s] for user [%s]", webhookID, userID)) } return webhook, nil @@ -115,7 +115,7 @@ func (repository *gormWebhookRepository) Delete(ctx context.Context, userID enti Where("id = ?", webhookID). Delete(&entities.Webhook{}).Error if err != nil { - return repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot delete webhook with ID [%s] and userID [%s]", webhookID, userID)) + return repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot delete webhook with ID [%s] and userID [%s]", webhookID, userID)) } return nil diff --git a/api/pkg/repositories/memory_attachment_repository.go b/api/pkg/repositories/memory_attachment_repository.go index eab19336a..d6070d65d 100644 --- a/api/pkg/repositories/memory_attachment_repository.go +++ b/api/pkg/repositories/memory_attachment_repository.go @@ -44,7 +44,7 @@ func (s *MemoryAttachmentRepository) Download(ctx context.Context, path string) value, ok := s.data.Load(path) if !ok { - return nil, s.tracer.WrapErrorSpan(span, stacktrace.NewErrorWithCode(ErrCodeNotFound, "attachment not found at path [%s]", path)) + return nil, s.tracer.WrapErrorSpan(span, stacktrace.NewErrorWithCodef(ErrCodeNotFound, "attachment not found at path [%s]", path)) } return value.([]byte), nil } diff --git a/api/pkg/repositories/mongo_heartbeat_monitor_repository.go b/api/pkg/repositories/mongo_heartbeat_monitor_repository.go index 2f6f71cdb..fea05e993 100644 --- a/api/pkg/repositories/mongo_heartbeat_monitor_repository.go +++ b/api/pkg/repositories/mongo_heartbeat_monitor_repository.go @@ -43,7 +43,7 @@ func (repository *mongoHeartbeatMonitorRepository) Store(ctx context.Context, mo _, err := repository.collection.InsertOne(ctx, monitor) if err != nil { - return repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot save heartbeat monitor with ID [%s]", monitor.ID)) + return repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot save heartbeat monitor with ID [%s]", monitor.ID)) } return nil @@ -64,10 +64,10 @@ func (repository *mongoHeartbeatMonitorRepository) Load(ctx context.Context, use var monitor entities.HeartbeatMonitor err := repository.collection.FindOne(ctx, filter).Decode(&monitor) if err == mongo.ErrNoDocuments { - return nil, repository.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCode(err, ErrCodeNotFound, "heartbeat monitor with userID [%s] and owner [%s] does not exist", userID, phoneNumber)) + return nil, repository.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCodef(err, ErrCodeNotFound, "heartbeat monitor with userID [%s] and owner [%s] does not exist", userID, phoneNumber)) } if err != nil { - return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot load heartbeat monitor with userID [%s] and owner [%s]", userID, phoneNumber)) + return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot load heartbeat monitor with userID [%s] and owner [%s]", userID, phoneNumber)) } return &monitor, nil @@ -87,7 +87,7 @@ func (repository *mongoHeartbeatMonitorRepository) Exists(ctx context.Context, u count, err := repository.collection.CountDocuments(ctx, filter) if err != nil { - return false, repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot check if heartbeat monitor exists with userID [%s] and monitor ID [%s]", userID, monitorID)) + return false, repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot check if heartbeat monitor exists with userID [%s] and monitor ID [%s]", userID, monitorID)) } return count > 0, nil @@ -108,7 +108,7 @@ func (repository *mongoHeartbeatMonitorRepository) UpdateQueueID(ctx context.Con _, err := repository.collection.UpdateOne(ctx, filter, update) if err != nil { - return repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot update heartbeat monitor ID [%s]", monitorID)) + return repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot update heartbeat monitor ID [%s]", monitorID)) } return nil @@ -128,7 +128,7 @@ func (repository *mongoHeartbeatMonitorRepository) Delete(ctx context.Context, u _, err := repository.collection.DeleteMany(ctx, filter) if err != nil { - return repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot delete heartbeat monitor with owner [%s] and userID [%s]", phoneNumber, userID)) + return repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot delete heartbeat monitor with owner [%s] and userID [%s]", phoneNumber, userID)) } return nil @@ -152,7 +152,7 @@ func (repository *mongoHeartbeatMonitorRepository) UpdatePhoneOnline(ctx context _, err := repository.collection.UpdateOne(ctx, filter, update) if err != nil { - return repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot update heartbeat monitor ID [%s] for user [%s]", monitorID, userID)) + return repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot update heartbeat monitor ID [%s] for user [%s]", monitorID, userID)) } return nil @@ -167,7 +167,7 @@ func (repository *mongoHeartbeatMonitorRepository) DeleteAllForUser(ctx context. _, err := repository.collection.DeleteMany(ctx, bson.D{{Key: "user_id", Value: string(userID)}}) if err != nil { - return repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot delete all [%T] for user with ID [%s]", &entities.HeartbeatMonitor{}, userID)) + return repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot delete all [%T] for user with ID [%s]", &entities.HeartbeatMonitor{}, userID)) } return nil diff --git a/api/pkg/repositories/mongo_heartbeat_repository.go b/api/pkg/repositories/mongo_heartbeat_repository.go index e97e696a2..44ed280db 100644 --- a/api/pkg/repositories/mongo_heartbeat_repository.go +++ b/api/pkg/repositories/mongo_heartbeat_repository.go @@ -42,7 +42,7 @@ func (repository *mongoHeartbeatRepository) Store(ctx context.Context, heartbeat _, err := repository.collection.InsertOne(ctx, heartbeat) if err != nil { - return repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot save heartbeat with ID [%s]", heartbeat.ID)) + return repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot save heartbeat with ID [%s]", heartbeat.ID)) } return nil @@ -71,13 +71,13 @@ func (repository *mongoHeartbeatRepository) Index(ctx context.Context, userID en cursor, err := repository.collection.Find(ctx, filter, opts) if err != nil { - return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot fetch heartbeats with owner [%s] and params [%+#v]", owner, params)) + return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot fetch heartbeats with owner [%s] and params [%+#v]", owner, params)) } defer cursor.Close(ctx) var heartbeats []entities.Heartbeat if err = cursor.All(ctx, &heartbeats); err != nil { - return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot decode heartbeats for owner [%s]", owner)) + return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot decode heartbeats for owner [%s]", owner)) } if heartbeats == nil { @@ -104,10 +104,10 @@ func (repository *mongoHeartbeatRepository) Last(ctx context.Context, userID ent var heartbeat entities.Heartbeat err := repository.collection.FindOne(ctx, filter, opts).Decode(&heartbeat) if err == mongo.ErrNoDocuments { - return nil, repository.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCode(err, ErrCodeNotFound, "heartbeat with userID [%s] and owner [%s] does not exist", userID, owner)) + return nil, repository.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCodef(err, ErrCodeNotFound, "heartbeat with userID [%s] and owner [%s] does not exist", userID, owner)) } if err != nil { - return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot load heartbeat with userID [%s] and owner [%s]", userID, owner)) + return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot load heartbeat with userID [%s] and owner [%s]", userID, owner)) } return &heartbeat, nil @@ -122,7 +122,7 @@ func (repository *mongoHeartbeatRepository) DeleteAllForUser(ctx context.Context _, err := repository.collection.DeleteMany(ctx, bson.D{{Key: "user_id", Value: string(userID)}}) if err != nil { - return repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot delete all [%T] for user with ID [%s]", &entities.Heartbeat{}, userID)) + return repository.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot delete all [%T] for user with ID [%s]", &entities.Heartbeat{}, userID)) } return nil diff --git a/api/pkg/repositories/mongodb.go b/api/pkg/repositories/mongodb.go index 74ea7446f..ebefc0f8b 100644 --- a/api/pkg/repositories/mongodb.go +++ b/api/pkg/repositories/mongodb.go @@ -52,7 +52,7 @@ func newMongoRegistry() *bson.Registry { func NewMongoDB(uri string) (*mongo.Database, error) { dbName, err := parseMongoDBName(uri) if err != nil { - return nil, stacktrace.Propagate(err, "cannot parse database name from MongoDB URI") + return nil, stacktrace.Propagatef(err, "cannot parse database name from MongoDB URI") } pingCtx, pingCancel := context.WithTimeout(context.Background(), 10*time.Second) @@ -68,11 +68,11 @@ func NewMongoDB(uri string) (*mongo.Database, error) { client, err := mongo.Connect(opts) if err != nil { - return nil, stacktrace.Propagate(err, "cannot connect to MongoDB Atlas") + return nil, stacktrace.Propagatef(err, "cannot connect to MongoDB Atlas") } if err = client.Ping(pingCtx, nil); err != nil { - return nil, stacktrace.Propagate(err, "cannot ping MongoDB Atlas") + return nil, stacktrace.Propagatef(err, "cannot ping MongoDB Atlas") } db := client.Database(dbName) @@ -81,7 +81,7 @@ func NewMongoDB(uri string) (*mongo.Database, error) { defer indexCancel() if err = createMongoIndexes(indexCtx, db); err != nil { - return nil, stacktrace.Propagate(err, "cannot create MongoDB indexes") + return nil, stacktrace.Propagatef(err, "cannot create MongoDB indexes") } return db, nil @@ -91,12 +91,12 @@ func NewMongoDB(uri string) (*mongo.Database, error) { func parseMongoDBName(uri string) (string, error) { parsed, err := url.Parse(uri) if err != nil { - return "", stacktrace.Propagate(err, "cannot parse MongoDB URI [%s]", uri) + return "", stacktrace.Propagatef(err, "cannot parse MongoDB URI [%s]", uri) } appName := parsed.Query().Get("appName") if appName == "" { - return "", stacktrace.NewError("MongoDB URI is missing the 'appName' query parameter which is used as the database name") + return "", stacktrace.NewErrorf("MongoDB URI is missing the 'appName' query parameter which is used as the database name") } return appName, nil @@ -110,7 +110,7 @@ func createMongoIndexes(ctx context.Context, db *mongo.Database) error { {Keys: bson.D{{Key: "user_id", Value: 1}, {Key: "owner", Value: 1}, {Key: "timestamp", Value: -1}}}, }) if err != nil { - return stacktrace.Propagate(err, "cannot create indexes on heartbeats collection") + return stacktrace.Propagatef(err, "cannot create indexes on heartbeats collection") } // Heartbeat monitors indexes @@ -119,7 +119,7 @@ func createMongoIndexes(ctx context.Context, db *mongo.Database) error { {Keys: bson.D{{Key: "user_id", Value: 1}, {Key: "owner", Value: 1}}}, }) if err != nil { - return stacktrace.Propagate(err, "cannot create indexes on heartbeat_monitors collection") + return stacktrace.Propagatef(err, "cannot create indexes on heartbeat_monitors collection") } return nil diff --git a/api/pkg/services/billing_service.go b/api/pkg/services/billing_service.go index 6b5749933..e94002d1d 100644 --- a/api/pkg/services/billing_service.go +++ b/api/pkg/services/billing_service.go @@ -55,13 +55,13 @@ func (service *BillingService) IsEntitledWithCount(ctx context.Context, userID e user, err := service.userRepository.Load(ctx, userID) if err != nil { - ctxLogger.Error(service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot load user with ID [%s], entitlement successful", userID))) + ctxLogger.Error(service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot load user with ID [%s], entitlement successful", userID))) return nil } usage, err := service.billingUsageRepository.GetCurrent(ctx, userID) if err != nil { - ctxLogger.Error(service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot load billing usage for user with ID [%s], entitlement successful", userID))) + ctxLogger.Error(service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot load billing usage for user with ID [%s], entitlement successful", userID))) return nil } @@ -102,18 +102,18 @@ func (service *BillingService) sendLimitExceededEmail(ctx context.Context, user email, err := service.emailFactory.UsageLimitExceeded(user, usage) if err != nil { - ctxLogger.Error(stacktrace.Propagate(err, "cannot create usage limit email for user [%s]", user.ID)) + ctxLogger.Error(stacktrace.Propagatef(err, "cannot create usage limit email for user [%s]", user.ID)) return } if err = service.mailer.Send(ctx, email); err != nil { - ctxLogger.Error(stacktrace.Propagate(err, "canot send usage limit exceeded notification to user [%s]", user.ID)) + ctxLogger.Error(stacktrace.Propagatef(err, "canot send usage limit exceeded notification to user [%s]", user.ID)) return } ctxLogger.Info(fmt.Sprintf("usage limit exceeded email sent to user [%s]", user.ID)) if err = service.cache.Set(ctx, key, "", time.Hour*12); err != nil { - ctxLogger.Error(stacktrace.Propagate(err, "cannot set item in redis with key [%s]", key)) + ctxLogger.Error(stacktrace.Propagatef(err, "cannot set item in redis with key [%s]", key)) } } @@ -141,7 +141,7 @@ func (service *BillingService) RegisterSentMessage(ctx context.Context, messageI ctxLogger := service.tracer.CtxLogger(service.logger, span) if err := service.billingUsageRepository.RegisterSentMessage(ctx, timestamp, userID); err != nil { - return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "could not register [sent] message with ID [%s] for user with ID [%s]", messageID, userID)) + return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "could not register [sent] message with ID [%s] for user with ID [%s]", messageID, userID)) } ctxLogger.Info(fmt.Sprintf("registered [sent] message with ID [%s] for user [%s]", messageID, userID)) @@ -157,7 +157,7 @@ func (service *BillingService) RegisterReceivedMessage(ctx context.Context, mess ctxLogger := service.tracer.CtxLogger(service.logger, span) if err := service.billingUsageRepository.RegisterReceivedMessage(ctx, timestamp, userID); err != nil { - return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "could not register [received] message with ID [%s] for user with ID [%s]", messageID, userID)) + return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "could not register [received] message with ID [%s] for user with ID [%s]", messageID, userID)) } ctxLogger.Info(fmt.Sprintf("registered [received] message with ID [%s] for user [%s]", messageID, userID)) @@ -171,7 +171,7 @@ func (service *BillingService) DeleteAllForUser(ctx context.Context, userID enti defer span.End() if err := service.billingUsageRepository.DeleteAllForUser(ctx, userID); err != nil { - return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "could not delete [entities.BillingUsage] for user with ID [%s]", userID)) + return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "could not delete [entities.BillingUsage] for user with ID [%s]", userID)) } ctxLogger.Info(fmt.Sprintf("deleted all [entities.BillingUsage] for user with ID [%s]", userID)) @@ -184,13 +184,13 @@ func (service *BillingService) sendUsageAlert(ctx context.Context, userID entiti user, err := service.userRepository.Load(ctx, userID) if err != nil { - ctxLogger.Error(service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot load user with ID [%s]", userID))) + ctxLogger.Error(service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot load user with ID [%s]", userID))) return } billingUsage, err := service.billingUsageRepository.GetCurrent(ctx, userID) if err != nil { - ctxLogger.Error(service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot load billing usage for user with ID [%s]", userID))) + ctxLogger.Error(service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot load billing usage for user with ID [%s]", userID))) return } @@ -200,12 +200,12 @@ func (service *BillingService) sendUsageAlert(ctx context.Context, userID entiti email, err := service.emailFactory.UsageLimitAlert(user, billingUsage) if err != nil { - ctxLogger.Error(stacktrace.Propagate(err, "cannot create usage alert email for user [%s]", user.ID)) + ctxLogger.Error(stacktrace.Propagatef(err, "cannot create usage alert email for user [%s]", user.ID)) return } if err = service.mailer.Send(ctx, email); err != nil { - ctxLogger.Error(stacktrace.Propagate(err, "canot send usage alert notification to user [%s]", user.ID)) + ctxLogger.Error(stacktrace.Propagatef(err, "canot send usage alert notification to user [%s]", user.ID)) } ctxLogger.Info(fmt.Sprintf("usage alert email sent to user [%s]", user.ID)) diff --git a/api/pkg/services/discord_service.go b/api/pkg/services/discord_service.go index e919109b3..68bbbdc7b 100644 --- a/api/pkg/services/discord_service.go +++ b/api/pkg/services/discord_service.go @@ -59,7 +59,7 @@ func (service *DiscordService) DeleteAllForUser(ctx context.Context, userID enti defer span.End() if err := service.repository.DeleteAllForUser(ctx, userID); err != nil { - return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "could not delete all [entities.Discord] for user with ID [%s]", userID)) + return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "could not delete all [entities.Discord] for user with ID [%s]", userID)) } ctxLogger.Info(fmt.Sprintf("deleted all [entities.Discord] for user with ID [%s]", userID)) @@ -75,7 +75,7 @@ func (service *DiscordService) Index(ctx context.Context, userID entities.UserID discordIntegrations, err := service.repository.Index(ctx, userID, params) if err != nil { - return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "could not fetch discord integrations with params [%+#v]", params)) + return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "could not fetch discord integrations with params [%+#v]", params)) } ctxLogger.Info(fmt.Sprintf("fetched [%d] discord integrations with prams [%+#v]", len(discordIntegrations), params)) @@ -90,11 +90,11 @@ func (service *DiscordService) Delete(ctx context.Context, userID entities.UserI ctxLogger := service.tracer.CtxLogger(service.logger, span) if _, err := service.repository.Load(ctx, userID, discordID); err != nil { - return service.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCode(err, stacktrace.GetCode(err), "cannot load discord integration with userID [%s] and discordID [%s]", userID, discordID)) + return service.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCodef(err, stacktrace.GetCode(err), "cannot load discord integration with userID [%s] and discordID [%s]", userID, discordID)) } if err := service.repository.Delete(ctx, userID, discordID); err != nil { - return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot delete discord integration with id [%s] and discordID [%s]", discordID, userID)) + return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot delete discord integration with id [%s] and discordID [%s]", discordID, userID)) } ctxLogger.Info(fmt.Sprintf("deleted discord integration with id [%s] and user id [%s]", discordID, userID)) @@ -115,7 +115,7 @@ func (service *DiscordService) Store(ctx context.Context, params *DiscordStorePa defer span.End() if err := service.createSlashCommand(ctx, params.ServerID); err != nil { - return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot create slash command for server [%s]", params.ServerID)) + return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot create slash command for server [%s]", params.ServerID)) } discordIntegration := &entities.Discord{ @@ -129,7 +129,7 @@ func (service *DiscordService) Store(ctx context.Context, params *DiscordStorePa } if err := service.repository.Save(ctx, discordIntegration); err != nil { - return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot save discord integration with id [%s]", discordIntegration.ID)) + return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot save discord integration with id [%s]", discordIntegration.ID)) } ctxLogger.Info(fmt.Sprintf("discord integration saved with id [%s] in the [%T]", discordIntegration.ID, service.repository)) @@ -172,7 +172,7 @@ func (service *DiscordService) createSlashCommand(ctx context.Context, serverID }, }) if err != nil { - return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot create slash command for server [%s]", serverID)) + return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot create slash command for server [%s]", serverID)) } ctxLogger.Info(fmt.Sprintf("upserted a slash command with ID [%s] for discord server [%s] and applicationID [%s]", command.ID, serverID, command.ApplicationID)) @@ -195,11 +195,11 @@ func (service *DiscordService) Update(ctx context.Context, params *DiscordUpdate discordIntegration, err := service.repository.Load(ctx, params.UserID, params.DiscordID) if err != nil { - return nil, service.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCode(err, stacktrace.GetCode(err), "cannot load discord integration with userID [%s] and discordID [%s]", params.UserID, params.DiscordID)) + return nil, service.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCodef(err, stacktrace.GetCode(err), "cannot load discord integration with userID [%s] and discordID [%s]", params.UserID, params.DiscordID)) } if err = service.createSlashCommand(ctx, params.ServerID); err != nil { - return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot create slash command for server [%s]", params.ServerID)) + return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot create slash command for server [%s]", params.ServerID)) } discordIntegration.Name = params.Name @@ -207,7 +207,7 @@ func (service *DiscordService) Update(ctx context.Context, params *DiscordUpdate discordIntegration.IncomingChannelID = params.IncomingChannelID if err = service.repository.Save(ctx, discordIntegration); err != nil { - return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot save discord integration with id [%s] after update", discordIntegration.ID)) + return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot save discord integration with id [%s] after update", discordIntegration.ID)) } ctxLogger.Info(fmt.Sprintf("discord integration updated with id [%s] in the [%T]", discordIntegration.ID, service.repository)) @@ -221,7 +221,7 @@ func (service *DiscordService) HandleMessageReceived(ctx context.Context, userID discordIntegrations, err := service.repository.FetchHavingIncomingChannel(ctx, userID) if err != nil { - return service.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCode(err, stacktrace.GetCode(err), "cannot load discord integrations for user with ID [%s]", userID)) + return service.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCodef(err, stacktrace.GetCode(err), "cannot load discord integrations for user with ID [%s]", userID)) } if len(discordIntegrations) == 0 { @@ -248,14 +248,14 @@ func (service *DiscordService) sendMessage(ctx context.Context, event cloudevent payload := new(events.MessagePhoneReceivedPayload) if err := event.DataAs(payload); err != nil { - ctxLogger.Error(stacktrace.Propagate(err, "cannot unmarshal event [%s] with ID [%s] into [%T]", event.Type(), event.ID(), payload)) + ctxLogger.Error(stacktrace.Propagatef(err, "cannot unmarshal event [%s] with ID [%s] into [%T]", event.Type(), event.ID(), payload)) return } request := service.createDiscordMessage(ctxLogger, payload) message, response, err := service.client.Channel.CreateMessage(ctx, discord.IncomingChannelID, request) if err != nil { - ctxLogger.Warn(service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot send [%s] event to discord channel [%s] for user [%s]", event.Type(), discord.IncomingChannelID, discord.UserID))) + ctxLogger.Warn(service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot send [%s] event to discord channel [%s] for user [%s]", event.Type(), discord.IncomingChannelID, discord.UserID))) eventPayload := &events.DiscordSendFailedPayload{ DiscordID: discord.ID, @@ -315,12 +315,12 @@ func (service *DiscordService) handleDiscordMessageFailed(ctx context.Context, s event, err := service.createEvent(events.EventTypeDiscordSendFailed, source, payload) if err != nil { - ctxLogger.Error(service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot create event [%s] for user with id [%s]", events.EventTypeDiscordSendFailed, payload.UserID))) + ctxLogger.Error(service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot create event [%s] for user with id [%s]", events.EventTypeDiscordSendFailed, payload.UserID))) return } if err = service.dispatcher.Dispatch(ctx, event); err != nil { - ctxLogger.Error(service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot dispatch event [%s] for user with id [%s]", event.Type(), payload.UserID))) + ctxLogger.Error(service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot dispatch event [%s] for user with id [%s]", event.Type(), payload.UserID))) return } diff --git a/api/pkg/services/email_notification_service.go b/api/pkg/services/email_notification_service.go index a87fcf227..d72b3ee0c 100644 --- a/api/pkg/services/email_notification_service.go +++ b/api/pkg/services/email_notification_service.go @@ -66,7 +66,7 @@ func (service *EmailNotificationService) NotifyMessageExpired(ctx context.Contex user, err := service.userRepository.Load(ctx, payload.UserID) if err != nil { - return service.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCode(err, stacktrace.GetCode(err), "cannot load user with ID [%s] and for expired message with ID [%s]", payload.UserID, payload.MessageID)) + return service.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCodef(err, stacktrace.GetCode(err), "cannot load user with ID [%s] and for expired message with ID [%s]", payload.UserID, payload.MessageID)) } if !user.NotificationMessageStatusEnabled { @@ -76,11 +76,11 @@ func (service *EmailNotificationService) NotifyMessageExpired(ctx context.Contex email, err := service.factory.MessageExpired(user, payload) if err != nil { - return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot create email for user with ID [%s] and for expired message with ID [%s]", payload.UserID, payload.MessageID)) + return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot create email for user with ID [%s] and for expired message with ID [%s]", payload.UserID, payload.MessageID)) } if err = service.mailer.Send(ctx, email); err != nil { - return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot send email for user with ID [%s] and for expired message with ID [%s]", payload.UserID, payload.MessageID)) + return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot send email for user with ID [%s] and for expired message with ID [%s]", payload.UserID, payload.MessageID)) } ctxLogger.Info(fmt.Sprintf("[%s] email sent to [%s] for message with ID [%s]", events.EventTypeMessageSendExpired, user.ID, payload.MessageID)) @@ -101,7 +101,7 @@ func (service *EmailNotificationService) NotifyMessageFailed(ctx context.Context user, err := service.userRepository.Load(ctx, payload.UserID) if err != nil { - return service.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCode(err, stacktrace.GetCode(err), "cannot load user with ID [%s] for [%s] message with ID [%s]", payload.UserID, events.EventTypeMessageSendFailed, payload.ID)) + return service.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCodef(err, stacktrace.GetCode(err), "cannot load user with ID [%s] for [%s] message with ID [%s]", payload.UserID, events.EventTypeMessageSendFailed, payload.ID)) } if !user.NotificationMessageStatusEnabled { @@ -111,11 +111,11 @@ func (service *EmailNotificationService) NotifyMessageFailed(ctx context.Context email, err := service.factory.MessageFailed(user, payload) if err != nil { - return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot create email for user with ID [%s] for [%s] message with ID [%s]", payload.UserID, events.EventTypeMessageSendFailed, payload.ID)) + return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot create email for user with ID [%s] for [%s] message with ID [%s]", payload.UserID, events.EventTypeMessageSendFailed, payload.ID)) } if err = service.mailer.Send(ctx, email); err != nil { - return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot send email for user with ID [%s] for [%s] message with ID [%s]", payload.UserID, events.EventTypeMessageSendFailed, payload.ID)) + return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot send email for user with ID [%s] for [%s] message with ID [%s]", payload.UserID, events.EventTypeMessageSendFailed, payload.ID)) } ctxLogger.Info(fmt.Sprintf("[%s] email sent to [%s] for message with ID [%s]", events.EventTypeMessageSendFailed, user.ID, payload.ID)) @@ -136,7 +136,7 @@ func (service *EmailNotificationService) NotifyWebhookSendFailed(ctx context.Con user, err := service.userRepository.Load(ctx, payload.UserID) if err != nil { - return service.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCode(err, stacktrace.GetCode(err), "cannot load user with ID [%s] for [%s] event with ID [%s]", payload.UserID, events.EventTypeWebhookSendFailed, payload.EventID)) + return service.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCodef(err, stacktrace.GetCode(err), "cannot load user with ID [%s] for [%s] event with ID [%s]", payload.UserID, events.EventTypeWebhookSendFailed, payload.EventID)) } if !user.NotificationWebhookEnabled { @@ -146,11 +146,11 @@ func (service *EmailNotificationService) NotifyWebhookSendFailed(ctx context.Con email, err := service.factory.WebhookSendFailed(user, payload) if err != nil { - return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot create [%s] email for user with ID [%s] for [%s] event with ID [%s]", events.EventTypeWebhookSendFailed, payload.UserID, payload.EventType, payload.EventID)) + return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot create [%s] email for user with ID [%s] for [%s] event with ID [%s]", events.EventTypeWebhookSendFailed, payload.UserID, payload.EventType, payload.EventID)) } if err = service.mailer.Send(ctx, email); err != nil { - return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot send [%s] email for user with ID [%s] for [%s] event with ID [%s]", events.EventTypeWebhookSendFailed, payload.UserID, payload.EventType, payload.EventID)) + return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot send [%s] email for user with ID [%s] for [%s] event with ID [%s]", events.EventTypeWebhookSendFailed, payload.UserID, payload.EventType, payload.EventID)) } ctxLogger.Info(fmt.Sprintf("[%s] email sent to [%s] for [%s] event with ID [%s]", events.EventTypeWebhookSendFailed, user.ID, payload.EventType, payload.EventID)) @@ -171,7 +171,7 @@ func (service *EmailNotificationService) NotifyDiscordSendFailed(ctx context.Con user, err := service.userRepository.Load(ctx, payload.UserID) if err != nil { - return service.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCode(err, stacktrace.GetCode(err), "cannot load user with ID [%s] for [%s] event for message with ID [%s]", payload.UserID, payload.EventType, payload.MessageID)) + return service.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCodef(err, stacktrace.GetCode(err), "cannot load user with ID [%s] for [%s] event for message with ID [%s]", payload.UserID, payload.EventType, payload.MessageID)) } if !user.NotificationWebhookEnabled { @@ -181,11 +181,11 @@ func (service *EmailNotificationService) NotifyDiscordSendFailed(ctx context.Con email, err := service.factory.DiscordSendFailed(user, payload) if err != nil { - return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot create email for user with ID [%s] for [%s] event and message with ID [%s]", payload.UserID, payload.EventType, payload.MessageID)) + return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot create email for user with ID [%s] for [%s] event and message with ID [%s]", payload.UserID, payload.EventType, payload.MessageID)) } if err = service.mailer.Send(ctx, email); err != nil { - return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot send email for user with ID [%s] for [%s] message with ID [%s]", payload.UserID, events.EventTypeMessageSendFailed, payload.MessageID)) + return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot send email for user with ID [%s] for [%s] message with ID [%s]", payload.UserID, events.EventTypeMessageSendFailed, payload.MessageID)) } ctxLogger.Info(fmt.Sprintf("[%s] email sent to [%s] for [%s] event with ID [%s]", payload.EventType, user.ID, payload.EventType, payload.MessageID)) @@ -209,6 +209,6 @@ func (service *EmailNotificationService) addToCache(ctx context.Context, timeout cacheKey := service.getCacheKey(event, owner) if err := service.cache.Set(ctx, cacheKey, "", timeout); err != nil { - ctxLogger.Error(stacktrace.Propagate(err, "cannot set item in redis with key [%s] for owner [%s]", cacheKey, owner)) + ctxLogger.Error(stacktrace.Propagatef(err, "cannot set item in redis with key [%s] for owner [%s]", cacheKey, owner)) } } diff --git a/api/pkg/services/emulator_fcm_client.go b/api/pkg/services/emulator_fcm_client.go index adf3ed7c5..6b890a14a 100644 --- a/api/pkg/services/emulator_fcm_client.go +++ b/api/pkg/services/emulator_fcm_client.go @@ -65,34 +65,34 @@ func (c *EmulatorFCMClient) Send(ctx context.Context, message *messaging.Message body, err := json.Marshal(payload) if err != nil { - return "", stacktrace.Propagate(err, "cannot marshal FCM request for emulator") + return "", stacktrace.Propagatef(err, "cannot marshal FCM request for emulator") } url := fmt.Sprintf("%s/v1/projects/httpsms-test/messages:send", c.endpoint) req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body)) if err != nil { - return "", stacktrace.Propagate(err, "cannot create HTTP request for emulator FCM") + return "", stacktrace.Propagatef(err, "cannot create HTTP request for emulator FCM") } req.Header.Set("Content-Type", "application/json") resp, err := c.httpClient.Do(req) if err != nil { - return "", stacktrace.Propagate(err, "cannot send FCM to emulator at [%s]", url) + return "", stacktrace.Propagatef(err, "cannot send FCM to emulator at [%s]", url) } defer resp.Body.Close() respBody, err := io.ReadAll(resp.Body) if err != nil { - return "", stacktrace.Propagate(err, "cannot read emulator FCM response body") + return "", stacktrace.Propagatef(err, "cannot read emulator FCM response body") } if resp.StatusCode != http.StatusOK { - return "", stacktrace.NewError("emulator FCM returned status %d: %s", resp.StatusCode, string(respBody)) + return "", stacktrace.NewErrorf("emulator FCM returned status %d: %s", resp.StatusCode, string(respBody)) } var result emulatorFCMResponse if err = json.Unmarshal(respBody, &result); err != nil { - return "", stacktrace.Propagate(err, "cannot decode emulator FCM response") + return "", stacktrace.Propagatef(err, "cannot decode emulator FCM response") } c.logger.Info(fmt.Sprintf("emulator FCM sent successfully: %s", result.Name)) diff --git a/api/pkg/services/emulator_push_queue.go b/api/pkg/services/emulator_push_queue.go index 3e99a2369..634689f1e 100644 --- a/api/pkg/services/emulator_push_queue.go +++ b/api/pkg/services/emulator_push_queue.go @@ -74,7 +74,7 @@ func (queue *emulatorPushQueue) push(task PushQueueTask, queueID string) func() request.Header("Content-Type", "application/json") if err := request.Fetch(ctx); err != nil { - queue.logger.Error(stacktrace.Propagate(err, "cannot send http request to [%s] for queue task [%s]", task.URL, queueID)) + queue.logger.Error(stacktrace.Propagatef(err, "cannot send http request to [%s] for queue task [%s]", task.URL, queueID)) return } diff --git a/api/pkg/services/entitlement_service.go b/api/pkg/services/entitlement_service.go index a4a4ab891..96a5af144 100644 --- a/api/pkg/services/entitlement_service.go +++ b/api/pkg/services/entitlement_service.go @@ -79,7 +79,7 @@ func (service *EntitlementService) Check( if err != nil { return nil, service.tracer.WrapErrorSpan( span, - stacktrace.Propagate(err, "cannot load user [%s] for entitlement check", userID), + stacktrace.Propagatef(err, "cannot load user [%s] for entitlement check", userID), ) } @@ -92,7 +92,7 @@ func (service *EntitlementService) Check( if err != nil { return nil, service.tracer.WrapErrorSpan( span, - stacktrace.Propagate(err, "cannot count entities [%s] for user [%s]", entityName, userID), + stacktrace.Propagatef(err, "cannot count entities [%s] for user [%s]", entityName, userID), ) } diff --git a/api/pkg/services/event_dispatcher_service.go b/api/pkg/services/event_dispatcher_service.go index a090c943a..245ab0b2d 100644 --- a/api/pkg/services/event_dispatcher_service.go +++ b/api/pkg/services/event_dispatcher_service.go @@ -53,7 +53,7 @@ func (dispatcher *EventDispatcher) DispatchSync(ctx context.Context, event cloud defer span.End() if err := event.Validate(); err != nil { - return dispatcher.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot dispatch event with ID [%s] and type [%s] because it is invalid", event.ID(), event.Type())) + return dispatcher.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot dispatch event with ID [%s] and type [%s] because it is invalid", event.ID(), event.Type())) } dispatcher.Publish(ctx, event) @@ -66,16 +66,16 @@ func (dispatcher *EventDispatcher) DispatchWithTimeout(ctx context.Context, even defer span.End() if err = event.Validate(); err != nil { - return queueID, dispatcher.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot dispatch event with ID [%s] and type [%s] because it is invalid", event.ID(), event.Type())) + return queueID, dispatcher.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot dispatch event with ID [%s] and type [%s] because it is invalid", event.ID(), event.Type())) } task, err := dispatcher.createCloudTask(event) if err != nil { - return queueID, dispatcher.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot create cloud task for event [%s] with id [%s]", event.Type(), event.ID())) + return queueID, dispatcher.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot create cloud task for event [%s] with id [%s]", event.Type(), event.ID())) } if queueID, err = dispatcher.enqueue(ctx, event, task, timeout); err != nil { - return queueID, dispatcher.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot enqueue event with ID [%s] and type [%s]", event.ID(), event.Type())) + return queueID, dispatcher.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot enqueue event with ID [%s] and type [%s]", event.ID(), event.Type())) } return queueID, nil @@ -87,7 +87,7 @@ func (dispatcher *EventDispatcher) enqueue(ctx context.Context, event cloudevent queueID, err := dispatcher.queue.Enqueue(ctx, task, timeout) if errors.Is(err, context.DeadlineExceeded) { - ctxLogger.Warn(stacktrace.Propagate(err, "cannot enqueue event with ID [%s] and type [%s] to [%T]", event.ID(), event.Type(), dispatcher.queue)) + ctxLogger.Warn(stacktrace.Propagatef(err, "cannot enqueue event with ID [%s] and type [%s] to [%T]", event.ID(), event.Type(), dispatcher.queue)) queueID, err = fmt.Sprintf("local-%s", event.ID()), nil time.AfterFunc(timeout, func() { dispatcher.Publish(ctx, event) @@ -133,7 +133,7 @@ func (dispatcher *EventDispatcher) Publish(ctx context.Context, event cloudevent wg.Add(1) go func(ctx context.Context, sub events.EventListener) { if err := sub(ctx, event); err != nil { - ctxLogger.Error(stacktrace.Propagate(err, "subscriber [%T] cannot handle event [%s]", sub, event.Type())) + ctxLogger.Error(stacktrace.Propagatef(err, "subscriber [%T] cannot handle event [%s]", sub, event.Type())) } wg.Done() }(ctx, sub) @@ -163,7 +163,7 @@ func (dispatcher *EventDispatcher) addCloudEventAttributes(span trace.Span, even func (dispatcher *EventDispatcher) createCloudTask(event cloudevents.Event) (*PushQueueTask, error) { eventContent, err := json.Marshal(event) if err != nil { - return nil, stacktrace.Propagate(err, "cannot marshall [%T] with ID [%s]", event, event.ID()) + return nil, stacktrace.Propagatef(err, "cannot marshall [%T] with ID [%s]", event, event.ID()) } return &PushQueueTask{ diff --git a/api/pkg/services/google_cloud_push_queue_service.go b/api/pkg/services/google_cloud_push_queue_service.go index 7f9555207..2208cf218 100644 --- a/api/pkg/services/google_cloud_push_queue_service.go +++ b/api/pkg/services/google_cloud_push_queue_service.go @@ -81,8 +81,8 @@ func (queue *googlePushQueue) enqueueImpl(ctx context.Context, task *PushQueueTa queueTask, err := queue.client.CreateTask(requestCtx, req) if err != nil { - ctxLogger.Error(stacktrace.Propagate(err, "cannot schedule task [%s] to URL [%s]", string(task.Body), task.URL)) - return queueID, queue.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot schedule task [%s] to URL [%s]", string(task.Body), task.URL)) + ctxLogger.Error(stacktrace.Propagatef(err, "cannot schedule task [%s] to URL [%s]", string(task.Body), task.URL)) + return queueID, queue.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot schedule task [%s] to URL [%s]", string(task.Body), task.URL)) } ctxLogger.Info(fmt.Sprintf( diff --git a/api/pkg/services/heartbeat_service.go b/api/pkg/services/heartbeat_service.go index 1f02e9c4b..7441d2aa9 100644 --- a/api/pkg/services/heartbeat_service.go +++ b/api/pkg/services/heartbeat_service.go @@ -55,11 +55,11 @@ func (service *HeartbeatService) DeleteAllForUser(ctx context.Context, userID en defer span.End() if err := service.repository.DeleteAllForUser(ctx, userID); err != nil { - return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "could not delete all [entities.Heartbeat] for user with ID [%s]", userID)) + return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "could not delete all [entities.Heartbeat] for user with ID [%s]", userID)) } if err := service.monitorRepository.DeleteAllForUser(ctx, userID); err != nil { - return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "could not delete all [entities.HeartbeatMonitor] for user with ID [%s]", userID)) + return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "could not delete all [entities.HeartbeatMonitor] for user with ID [%s]", userID)) } ctxLogger.Info(fmt.Sprintf("deleted all [entities.Heartbeat] and [entities.HeartbeatMonitor] for user with ID [%s]", userID)) @@ -75,7 +75,7 @@ func (service *HeartbeatService) Index(ctx context.Context, userID entities.User heartbeats, err := service.repository.Index(ctx, userID, owner, params) if err != nil { - return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "could not fetch heartbeats with parms [%+#v]", params)) + return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "could not fetch heartbeats with parms [%+#v]", params)) } ctxLogger.Info(fmt.Sprintf("fetched [%d] messages with prams [%+#v]", len(*heartbeats), params)) @@ -109,7 +109,7 @@ func (service *HeartbeatService) Store(ctx context.Context, params HeartbeatStor } if err := service.repository.Store(ctx, heartbeat); err != nil { - return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot save heartbeat with id [%s]", heartbeat.ID)) + return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot save heartbeat with id [%s]", heartbeat.ID)) } ctxLogger.Info(fmt.Sprintf("heartbeat saved with id [%s] for user [%s]", heartbeat.ID, heartbeat.UserID)) @@ -120,7 +120,7 @@ func (service *HeartbeatService) Store(ctx context.Context, params HeartbeatStor return heartbeat, nil } if err != nil { - ctxLogger.Error(stacktrace.Propagate(err, "cannot load heartbeat monitor for owner [%s] and user [%s]", params.Owner, params.UserID)) + ctxLogger.Error(stacktrace.Propagatef(err, "cannot load heartbeat monitor for owner [%s] and user [%s]", params.Owner, params.UserID)) return heartbeat, nil } @@ -145,7 +145,7 @@ func (service *HeartbeatService) handleHeartbeatWhenPhoneWasOffline(ctx context. defer span.End() if err := service.UpdatePhoneOnline(ctx, monitor.UserID, monitor.ID, true); err != nil { - ctxLogger.Error(service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot update phone online status for heartbeat monitor [%s]", monitor.ID))) + ctxLogger.Error(service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot update phone online status for heartbeat monitor [%s]", monitor.ID))) } event, err := service.createEvent(events.EventTypePhoneHeartbeatOnline, source, &events.PhoneHeartbeatOnlinePayload{ @@ -157,12 +157,12 @@ func (service *HeartbeatService) handleHeartbeatWhenPhoneWasOffline(ctx context. Owner: heartbeat.Owner, }) if err != nil { - ctxLogger.Error(service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot create [%s] event for monitor with ID [%s]", events.EventTypePhoneHeartbeatOnline, monitor.ID))) + ctxLogger.Error(service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot create [%s] event for monitor with ID [%s]", events.EventTypePhoneHeartbeatOnline, monitor.ID))) return } if err = service.dispatcher.Dispatch(ctx, event); err != nil { - ctxLogger.Error(service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot dispatch event [%s] for heartbeat monitor with phone id [%s]", event.Type(), monitor.PhoneID))) + ctxLogger.Error(service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot dispatch event [%s] for heartbeat monitor with phone id [%s]", event.Type(), monitor.PhoneID))) return } @@ -177,7 +177,7 @@ func (service *HeartbeatService) StoreMonitor(ctx context.Context, params *Heart monitor, scheduleCheck, err := service.phoneMonitor(ctx, params) if err != nil { - return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot create monitor for with userID [%s] and owner [%s]", params.UserID, params.Owner)) + return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot create monitor for with userID [%s] and owner [%s]", params.UserID, params.Owner)) } if !scheduleCheck { @@ -195,7 +195,7 @@ func (service *HeartbeatService) StoreMonitor(ctx context.Context, params *Heart Source: params.Source, } if err = service.scheduleHeartbeatCheck(ctx, time.Now().UTC(), monitorParams); err != nil { - return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot schedule healthcheck for monitor [%s] with owner [%s] and userID [%s]", monitor.ID, params.Owner, params.UserID)) + return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot schedule healthcheck for monitor [%s] with owner [%s] and userID [%s]", monitor.ID, params.Owner, params.UserID)) } return monitor, nil @@ -218,7 +218,7 @@ func (service *HeartbeatService) phoneMonitor(ctx context.Context, params *Heart } if err = service.monitorRepository.Store(ctx, monitor); err != nil { - return nil, false, service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot save heartbeat monitor for owner [%s] and user [%s]", monitor.Owner, monitor.UserID)) + return nil, false, service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot save heartbeat monitor for owner [%s] and user [%s]", monitor.Owner, monitor.UserID)) } ctxLogger.Info(fmt.Sprintf("heartbeat monitor saved with id [%s] for owner [%s] and user [%s]", monitor.ID, monitor.Owner, monitor.UserID)) @@ -226,7 +226,7 @@ func (service *HeartbeatService) phoneMonitor(ctx context.Context, params *Heart } if err != nil { - return nil, false, service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot check if monitor exists with userID [%s] and owner [%s]", params.UserID, params.Owner)) + return nil, false, service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot check if monitor exists with userID [%s] and owner [%s]", params.UserID, params.Owner)) } return monitor, monitor.RequiresCheck(), nil @@ -240,7 +240,7 @@ func (service *HeartbeatService) DeleteMonitor(ctx context.Context, userID entit ctxLogger := service.tracer.CtxLogger(service.logger, span) if err := service.monitorRepository.Delete(ctx, userID, owner); err != nil { - return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot delete heartbeat monitor with userID [%s] and owner [%s]", userID, owner)) + return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot delete heartbeat monitor with userID [%s] and owner [%s]", userID, owner)) } ctxLogger.Info(fmt.Sprintf("heartbeat monitor deleted for userID [%s] and owner [%s]", userID, owner)) @@ -255,7 +255,7 @@ func (service *HeartbeatService) UpdatePhoneOnline(ctx context.Context, userID e ctxLogger := service.tracer.CtxLogger(service.logger, span) if err := service.monitorRepository.UpdatePhoneOnline(ctx, userID, monitorID, phoneOnline); err != nil { - return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot update heartbeat monitor [%s] with userID [%s] and status [%t]", monitorID, userID, phoneOnline)) + return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot update heartbeat monitor [%s] with userID [%s] and status [%t]", monitorID, userID, phoneOnline)) } ctxLogger.Info(fmt.Sprintf("heartbeat monitor [%s] updated for userID [%s] and status [%t]", monitorID, userID, phoneOnline)) @@ -285,7 +285,7 @@ func (service *HeartbeatService) Monitor(ctx context.Context, params *HeartbeatM } if err != nil { - ctxLogger.Error(stacktrace.Propagate(err, "cannot check if monitor exists with userID [%s] and owner [%s]", params.UserID, params.Owner)) + ctxLogger.Error(stacktrace.Propagatef(err, "cannot check if monitor exists with userID [%s] and owner [%s]", params.UserID, params.Owner)) return service.scheduleHeartbeatCheck(ctx, time.Now().UTC(), params) } @@ -295,7 +295,7 @@ func (service *HeartbeatService) Monitor(ctx context.Context, params *HeartbeatM heartbeat, err := service.repository.Last(ctx, params.UserID, params.Owner) if err != nil { - ctxLogger.Error(stacktrace.Propagate(err, "cannot fetch last heartbeat for userID [%s] and owner [%s] and ID [%s] removing check", params.UserID, params.Owner, params.MonitorID)) + ctxLogger.Error(stacktrace.Propagatef(err, "cannot fetch last heartbeat for userID [%s] and owner [%s] and ID [%s] removing check", params.UserID, params.Owner, params.MonitorID)) return nil } @@ -326,12 +326,12 @@ func (service *HeartbeatService) handleMissedMonitor(ctx context.Context, lastTi Owner: params.Owner, }) if err != nil { - ctxLogger.Error(service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot create event when phone monitor [%s] missed heartbeat", params.MonitorID.String()))) + ctxLogger.Error(service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot create event when phone monitor [%s] missed heartbeat", params.MonitorID.String()))) return } if _, err = service.dispatcher.DispatchWithTimeout(ctx, event, heartbeatCheckInterval); err != nil { - ctxLogger.Error(service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot dispatch event [%s] for heartbeat monitor with phone id [%s]", event.Type(), params.PhoneID))) + ctxLogger.Error(service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot dispatch event [%s] for heartbeat monitor with phone id [%s]", event.Type(), params.PhoneID))) } } @@ -341,7 +341,7 @@ func (service *HeartbeatService) handleFailedMonitor(ctx context.Context, lastTi err := service.scheduleHeartbeatCheck(ctx, time.Now().UTC(), params) if err != nil { - return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot schedule healthcheck for monitor with owner [%s] and userID [%s]", params.Owner, params.UserID)) + return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot schedule healthcheck for monitor with owner [%s] and userID [%s]", params.Owner, params.UserID)) } event, err := service.createPhoneHeartbeatOfflineEvent(params.Source, &events.PhoneHeartbeatOfflinePayload{ @@ -353,11 +353,11 @@ func (service *HeartbeatService) handleFailedMonitor(ctx context.Context, lastTi Owner: params.Owner, }) if err != nil { - return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot create event when phone monitor failed")) + return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot create event when phone monitor failed")) } if err = service.dispatcher.Dispatch(ctx, event); err != nil { - return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot dispatch event [%s] for heartbeat monitor with phone id [%s]", event.Type(), params.PhoneID)) + return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot dispatch event [%s] for heartbeat monitor with phone id [%s]", event.Type(), params.PhoneID)) } ctxLogger.Info(fmt.Sprintf("heartbeat monitor with id [%s] and phone id [%s] failed for user [%s]", params.MonitorID, params.PhoneID, params.UserID)) @@ -376,16 +376,16 @@ func (service *HeartbeatService) scheduleHeartbeatCheck(ctx context.Context, las Owner: params.Owner, }) if err != nil { - return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot create event when phone monitor failed")) + return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot create event when phone monitor failed")) } queueID, err := service.dispatcher.DispatchWithTimeout(ctx, event, heartbeatCheckInterval) if err != nil { - return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot dispatch event [%s] for heartbeat monitor with phone id [%s]", event.Type(), params.PhoneID)) + return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot dispatch event [%s] for heartbeat monitor with phone id [%s]", event.Type(), params.PhoneID)) } if err = service.monitorRepository.UpdateQueueID(ctx, params.MonitorID, queueID); err != nil { - service.logger.Error(stacktrace.Propagate(err, "cannot update monitor with id [%s] with queue with ID [%s]", params.MonitorID, queueID)) + service.logger.Error(stacktrace.Propagatef(err, "cannot update monitor with id [%s] with queue with ID [%s]", params.MonitorID, queueID)) } ctxLogger.Info(fmt.Sprintf("heartbeat check scheduled for monitor with id [%s] and phone id [%s] and queue id [%s] for user [%s]", params.MonitorID, params.PhoneID, queueID, params.UserID)) diff --git a/api/pkg/services/integration_3cx_service.go b/api/pkg/services/integration_3cx_service.go index b3d046ef7..4b87d39f8 100644 --- a/api/pkg/services/integration_3cx_service.go +++ b/api/pkg/services/integration_3cx_service.go @@ -49,7 +49,7 @@ func (service *Integration3CXService) DeleteAllForUser(ctx context.Context, user defer span.End() if err := service.repository.DeleteAllForUser(ctx, userID); err != nil { - return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "could not delete all [entities.Integration3CX] for user with ID [%s]", userID)) + return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "could not delete all [entities.Integration3CX] for user with ID [%s]", userID)) } ctxLogger.Info(fmt.Sprintf("deleted all [entities.Integration3CX] for user with ID [%s]", userID)) @@ -68,7 +68,7 @@ func (service *Integration3CXService) Send(ctx context.Context, userID entities. } if err != nil { - return service.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCode(err, stacktrace.GetCode(err), "cannot load [3cx] integration for user [%s] and event [%s]", userID, event.Type())) + return service.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCodef(err, stacktrace.GetCode(err), "cannot load [3cx] integration for user [%s] and event [%s]", userID, event.Type())) } service.sendNotification(ctx, event, webhooks) @@ -84,30 +84,30 @@ func (service *Integration3CXService) sendNotification(ctx context.Context, even payload, err := service.getPayload(event) if err != nil { - ctxLogger.Error(service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot generate [3cx] payload from [%s] event with ID [%s] for user [%s]", event.Type(), event.ID(), integration.UserID))) + ctxLogger.Error(service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot generate [3cx] payload from [%s] event with ID [%s] for user [%s]", event.Type(), event.ID(), integration.UserID))) return } body, err := json.Marshal(payload) if err != nil { - ctxLogger.Error(service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot marshal [%T] for [%s] event with ID [%s] for user [%s]", payload, event.Type(), event.ID(), integration.UserID))) + ctxLogger.Error(service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot marshal [%T] for [%s] event with ID [%s] for user [%s]", payload, event.Type(), event.ID(), integration.UserID))) } request, err := http.NewRequestWithContext(ctx, http.MethodPost, integration.WebhookURL, bytes.NewBuffer(body)) if err != nil { - ctxLogger.Error(service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot create [%T] for [%s] event with ID [%s] for user [%s]", request, event.Type(), event.ID(), integration.UserID))) + ctxLogger.Error(service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot create [%T] for [%s] event with ID [%s] for user [%s]", request, event.Type(), event.ID(), integration.UserID))) return } request.Header.Set("Content-Type", "application/json; charset=UTF-8") response, err := service.client.Do(request) if err != nil { - ctxLogger.Error(service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot send [%s] event to [3cx] webhook [%s] for user [%s]", event.Type(), integration.WebhookURL, integration.UserID))) + ctxLogger.Error(service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot send [%s] event to [3cx] webhook [%s] for user [%s]", event.Type(), integration.WebhookURL, integration.UserID))) } defer func() { err = response.Body.Close() if err != nil { - ctxLogger.Error(stacktrace.Propagate(err, "cannot close response body")) + ctxLogger.Error(stacktrace.Propagatef(err, "cannot close response body")) } }() @@ -135,14 +135,14 @@ func (service *Integration3CXService) getPayload(event cloudevents.Event) (fiber case events.EventTypeMessageSendFailed: return service.getMessageSendFailedPayload(event) default: - return nil, stacktrace.NewError("cannot generate [3cx] payload for event [%s] with ID [%s]", event.Type(), event.ID()) + return nil, stacktrace.NewErrorf("cannot generate [3cx] payload for event [%s] with ID [%s]", event.Type(), event.ID()) } } func (service *Integration3CXService) getEventDeliveredPayload(event cloudevents.Event) (fiber.Map, error) { payload := new(events.MessagePhoneDeliveredPayload) if err := event.DataAs(payload); err != nil { - return nil, stacktrace.Propagate(err, "cannot unmarshal event [%s] with ID [%s] into [%T]", event.Type(), event.ID(), payload) + return nil, stacktrace.Propagatef(err, "cannot unmarshal event [%s] with ID [%s] into [%T]", event.Type(), event.ID(), payload) } return fiber.Map{ @@ -172,7 +172,7 @@ func (service *Integration3CXService) getEventDeliveredPayload(event cloudevents func (service *Integration3CXService) getMessageSentPayload(event cloudevents.Event) (fiber.Map, error) { payload := new(events.MessagePhoneSentPayload) if err := event.DataAs(payload); err != nil { - return nil, stacktrace.Propagate(err, "cannot unmarshal event [%s] with ID [%s] into [%T]", event.Type(), event.ID(), payload) + return nil, stacktrace.Propagatef(err, "cannot unmarshal event [%s] with ID [%s] into [%T]", event.Type(), event.ID(), payload) } return fiber.Map{ @@ -202,7 +202,7 @@ func (service *Integration3CXService) getMessageSentPayload(event cloudevents.Ev func (service *Integration3CXService) getMessageSendFailedPayload(event cloudevents.Event) (fiber.Map, error) { payload := new(events.MessageSendFailedPayload) if err := event.DataAs(payload); err != nil { - return nil, stacktrace.Propagate(err, "cannot unmarshal event [%s] with ID [%s] into [%T]", event.Type(), event.ID(), payload) + return nil, stacktrace.Propagatef(err, "cannot unmarshal event [%s] with ID [%s] into [%T]", event.Type(), event.ID(), payload) } return fiber.Map{ @@ -232,7 +232,7 @@ func (service *Integration3CXService) getMessageSendFailedPayload(event cloudeve func (service *Integration3CXService) getMessageReceivedPayload(event cloudevents.Event) (fiber.Map, error) { payload := new(events.MessagePhoneReceivedPayload) if err := event.DataAs(payload); err != nil { - return nil, stacktrace.Propagate(err, "cannot unmarshal event [%s] with ID [%s] into [%T]", event.Type(), event.ID(), payload) + return nil, stacktrace.Propagatef(err, "cannot unmarshal event [%s] with ID [%s] into [%T]", event.Type(), event.ID(), payload) } return fiber.Map{ diff --git a/api/pkg/services/lemonsqueezy_service.go b/api/pkg/services/lemonsqueezy_service.go index 48347335c..00df1fcd4 100644 --- a/api/pkg/services/lemonsqueezy_service.go +++ b/api/pkg/services/lemonsqueezy_service.go @@ -52,7 +52,7 @@ func (service *LemonsqueezyService) GetUserID(ctx context.Context, request *lemo ctxLogger.Info(fmt.Sprintf("user_id not found in request meta data. Searching via email [%s]", request.Data.Attributes.UserEmail)) user, err := service.userRepository.LoadByEmail(ctx, request.Data.Attributes.UserEmail) if err != nil { - return "", service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot load user with email [%s]", request.Data.Attributes.UserEmail)) + return "", service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot load user with email [%s]", request.Data.Attributes.UserEmail)) } return user.ID, nil @@ -65,7 +65,7 @@ func (service *LemonsqueezyService) HandleSubscriptionCreatedEvent(ctx context.C userID, err := service.GetUserID(ctx, request) if err != nil { - return stacktrace.Propagate(err, "cannot get user ID for subscription created event with ID [%s]", request.Data.ID) + return stacktrace.Propagatef(err, "cannot get user ID for subscription created event with ID [%s]", request.Data.ID) } payload := &events.UserSubscriptionCreatedPayload{ @@ -79,11 +79,11 @@ func (service *LemonsqueezyService) HandleSubscriptionCreatedEvent(ctx context.C event, err := service.createEvent(events.UserSubscriptionCreated, source, payload) if err != nil { - return stacktrace.Propagate(err, "cannot create [%s] event for user [%s]", events.UserSubscriptionCreated, payload.UserID) + return stacktrace.Propagatef(err, "cannot create [%s] event for user [%s]", events.UserSubscriptionCreated, payload.UserID) } if err = service.eventDispatcher.Dispatch(ctx, event); err != nil { - return stacktrace.Propagate(err, "cannot dispatch [%s] event for user [%s]", events.UserSubscriptionCreated, payload.UserID) + return stacktrace.Propagatef(err, "cannot dispatch [%s] event for user [%s]", events.UserSubscriptionCreated, payload.UserID) } ctxLogger.Info(fmt.Sprintf("[%s] subscription [%s] created for user [%s]", payload.SubscriptionName, payload.SubscriptionID, payload.UserID)) return nil @@ -96,7 +96,7 @@ func (service *LemonsqueezyService) HandleSubscriptionCanceledEvent(ctx context. user, err := service.userRepository.LoadBySubscriptionID(ctx, request.Data.ID) if err != nil { - return stacktrace.Propagate(err, "cannot load user with subscription ID [%s]", request.Data.ID) + return stacktrace.Propagatef(err, "cannot load user with subscription ID [%s]", request.Data.ID) } payload := &events.UserSubscriptionCancelledPayload{ @@ -110,11 +110,11 @@ func (service *LemonsqueezyService) HandleSubscriptionCanceledEvent(ctx context. event, err := service.createEvent(events.UserSubscriptionCancelled, source, payload) if err != nil { - return stacktrace.Propagate(err, "cannot created [%s] event for user [%s]", events.UserSubscriptionCancelled, payload.UserID) + return stacktrace.Propagatef(err, "cannot created [%s] event for user [%s]", events.UserSubscriptionCancelled, payload.UserID) } if err = service.eventDispatcher.Dispatch(ctx, event); err != nil { - return stacktrace.Propagate(err, "cannot dispatch [%s] event for user [%s]", events.UserSubscriptionCancelled, payload.UserID) + return stacktrace.Propagatef(err, "cannot dispatch [%s] event for user [%s]", events.UserSubscriptionCancelled, payload.UserID) } ctxLogger.Info(fmt.Sprintf("[%s] subscription [%s] cancelled for user [%s]", payload.SubscriptionName, payload.SubscriptionID, payload.UserID)) return nil @@ -132,7 +132,7 @@ func (service *LemonsqueezyService) HandleSubscriptionUpdatedEvent(ctx context.C user, err := service.userRepository.LoadBySubscriptionID(ctx, request.Data.ID) if err != nil { - return stacktrace.Propagate(err, "cannot load user with subscription ID [%s]", request.Data.ID) + return stacktrace.Propagatef(err, "cannot load user with subscription ID [%s]", request.Data.ID) } payload := &events.UserSubscriptionUpdatedPayload{ @@ -147,11 +147,11 @@ func (service *LemonsqueezyService) HandleSubscriptionUpdatedEvent(ctx context.C event, err := service.createEvent(events.UserSubscriptionUpdated, source, payload) if err != nil { - return stacktrace.Propagate(err, "cannot created [%s] event for user [%s]", events.UserSubscriptionUpdated, payload.UserID) + return stacktrace.Propagatef(err, "cannot created [%s] event for user [%s]", events.UserSubscriptionUpdated, payload.UserID) } if err = service.eventDispatcher.Dispatch(ctx, event); err != nil { - return stacktrace.Propagate(err, "cannot dispatch [%s] event for user [%s]", event.Type(), payload.UserID) + return stacktrace.Propagatef(err, "cannot dispatch [%s] event for user [%s]", event.Type(), payload.UserID) } ctxLogger.Info(fmt.Sprintf("[%s] subscription [%s] updated for user [%s]", payload.SubscriptionName, payload.SubscriptionID, payload.UserID)) return nil @@ -164,7 +164,7 @@ func (service *LemonsqueezyService) HandleSubscriptionExpiredEvent(ctx context.C user, err := service.userRepository.LoadBySubscriptionID(ctx, request.Data.ID) if err != nil { - return stacktrace.Propagate(err, "cannot load user with subscription ID [%s]", request.Data.ID) + return stacktrace.Propagatef(err, "cannot load user with subscription ID [%s]", request.Data.ID) } payload := &events.UserSubscriptionExpiredPayload{ @@ -179,11 +179,11 @@ func (service *LemonsqueezyService) HandleSubscriptionExpiredEvent(ctx context.C event, err := service.createEvent(events.UserSubscriptionExpired, source, payload) if err != nil { - return stacktrace.Propagate(err, "cannot created [%s] event for user [%s]", events.UserSubscriptionExpired, payload.UserID) + return stacktrace.Propagatef(err, "cannot created [%s] event for user [%s]", events.UserSubscriptionExpired, payload.UserID) } if err = service.eventDispatcher.Dispatch(ctx, event); err != nil { - return stacktrace.Propagate(err, "cannot dispatch [%s] event for user [%s]", event.Type(), payload.UserID) + return stacktrace.Propagatef(err, "cannot dispatch [%s] event for user [%s]", event.Type(), payload.UserID) } ctxLogger.Info(fmt.Sprintf("[%s] subscription [%s] expired for user [%s]", payload.SubscriptionName, payload.SubscriptionID, payload.UserID)) return nil @@ -229,6 +229,6 @@ func (service *LemonsqueezyService) subscriptionName(variant string) entities.Su } } - service.logger.Warn(stacktrace.NewError("unknown subscription variant [%s], defaulting to free", variant)) + service.logger.Warn(stacktrace.NewErrorf("unknown subscription variant [%s], defaulting to free", variant)) return entities.SubscriptionNameFree } diff --git a/api/pkg/services/marketting_service.go b/api/pkg/services/marketting_service.go index aabc2d94a..65f608543 100644 --- a/api/pkg/services/marketting_service.go +++ b/api/pkg/services/marketting_service.go @@ -45,7 +45,7 @@ func (service *MarketingService) DeleteContact(ctx context.Context, email string response, _, err := service.plunkClient.Contacts.List(ctx, map[string]string{"search": email}) if err != nil { - return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot search for contact with email [%s]", email)) + return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot search for contact with email [%s]", email)) } if len(response.Data) == 0 { @@ -55,7 +55,7 @@ func (service *MarketingService) DeleteContact(ctx context.Context, email string contact := response.Data[0] if _, err = service.plunkClient.Contacts.Delete(ctx, contact.ID); err != nil { - return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot delete user with ID [%s] from contacts", contact.Data[string(semconv.EnduserIDKey)])) + return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot delete user with ID [%s] from contacts", contact.Data[string(semconv.EnduserIDKey)])) } ctxLogger.Info(fmt.Sprintf("deleted user with ID [%s] from as marketting contact with ID [%s]", contact.Data[string(semconv.EnduserIDKey)], contact.ID)) @@ -69,7 +69,7 @@ func (service *MarketingService) CreateContact(ctx context.Context, userID entit userRecord, err := service.authClient.GetUser(ctx, userID.String()) if err != nil { - return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot get auth user with id [%s]", userID)) + return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot get auth user with id [%s]", userID)) } data := service.attributes(userRecord) @@ -83,7 +83,7 @@ func (service *MarketingService) CreateContact(ctx context.Context, userID entit Data: data, }) if err != nil { - return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot create contact for user with id [%s]", userID)) + return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot create contact for user with id [%s]", userID)) } ctxLogger.Info(fmt.Sprintf("user [%s] added to marketting list with contact ID [%s] and event ID [%s]", userID, event.Data.Contact, event.Data.Event)) diff --git a/api/pkg/services/message_send_schedule_service.go b/api/pkg/services/message_send_schedule_service.go index 85627116e..e9e128f03 100644 --- a/api/pkg/services/message_send_schedule_service.go +++ b/api/pkg/services/message_send_schedule_service.go @@ -92,7 +92,7 @@ func (service *MessageSendScheduleService) Store( if err := service.repository.Store(ctx, schedule); err != nil { return nil, service.tracer.WrapErrorSpan( span, - stacktrace.Propagate( + stacktrace.Propagatef( err, "cannot store message send schedule [%s]", schedule.ID, ), @@ -125,7 +125,7 @@ func (service *MessageSendScheduleService) Update( if err = service.repository.Update(ctx, schedule); err != nil { return nil, service.tracer.WrapErrorSpan( span, - stacktrace.Propagate( + stacktrace.Propagatef( err, "cannot update message send schedule [%s]", schedule.ID, ), @@ -145,7 +145,7 @@ func (service *MessageSendScheduleService) Delete( defer span.End() if err := service.repository.Delete(ctx, userID, scheduleID); err != nil { - return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot delete message send schedule with ID [%s] for user [%s]", scheduleID, userID)) + return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot delete message send schedule with ID [%s] for user [%s]", scheduleID, userID)) } event, err := service.createEvent(events.EventTypeMessageSendScheduleDeleted, fmt.Sprintf("%T", service), events.MessageSendScheduleDeletedPayload{ @@ -154,11 +154,11 @@ func (service *MessageSendScheduleService) Delete( Timestamp: time.Now().UTC(), }) if err != nil { - return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot create [%s] event for schedule [%s]", events.EventTypeMessageSendScheduleDeleted, scheduleID)) + return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot create [%s] event for schedule [%s]", events.EventTypeMessageSendScheduleDeleted, scheduleID)) } if err = service.dispatcher.Dispatch(ctx, event); err != nil { - return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot dispatch [%s] event for schedule [%s]", event.Type(), scheduleID)) + return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot dispatch [%s] event for schedule [%s]", event.Type(), scheduleID)) } return nil @@ -199,7 +199,7 @@ func (service *MessageSendScheduleService) DeleteAllForUser( if err := service.repository.DeleteAllForUser(ctx, userID); err != nil { return service.tracer.WrapErrorSpan( span, - stacktrace.Propagate( + stacktrace.Propagatef( err, "cannot delete message send schedules for user [%s]", userID, ), diff --git a/api/pkg/services/message_service.go b/api/pkg/services/message_service.go index 4e6c91128..89c216817 100644 --- a/api/pkg/services/message_service.go +++ b/api/pkg/services/message_service.go @@ -79,7 +79,7 @@ func (service *MessageService) GetOutstanding(ctx context.Context, params Messag message, err := service.repository.GetOutstanding(ctx, params.UserID, params.MessageID, params.PhoneNumbers) if err != nil { - return nil, service.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCode(err, stacktrace.GetCode(err), "could not fetch outstanding messages with params [%s]", spew.Sdump(params))) + return nil, service.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCodef(err, stacktrace.GetCode(err), "could not fetch outstanding messages with params [%s]", spew.Sdump(params))) } event, err := service.createMessagePhoneSendingEvent(params.Source, events.MessagePhoneSendingPayload{ @@ -93,13 +93,13 @@ func (service *MessageService) GetOutstanding(ctx context.Context, params Messag SIM: message.SIM, }) if err != nil { - return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot create [%T] for message with ID [%s]", event, message.ID)) + return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot create [%T] for message with ID [%s]", event, message.ID)) } ctxLogger.Info(fmt.Sprintf("created event [%s] with id [%s] for message [%s]", event.Type(), event.ID(), message.ID)) if err = service.eventDispatcher.Dispatch(ctx, event); err != nil { - return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot dispatch event [%s] with id [%s] for message [%s]", event.Type(), event.ID(), message.ID)) + return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot dispatch event [%s] with id [%s] for message [%s]", event.Type(), event.ID(), message.ID)) } ctxLogger.Info(fmt.Sprintf("dispatched event [%s] with id [%s] for message [%s]", event.Type(), event.ID(), message.ID)) @@ -112,7 +112,7 @@ func (service *MessageService) DeleteAllForUser(ctx context.Context, userID enti defer span.End() if err := service.repository.DeleteAllForUser(ctx, userID); err != nil { - return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "could not delete [entities.Message] for user with ID [%s]", userID)) + return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "could not delete [entities.Message] for user with ID [%s]", userID)) } ctxLogger.Info(fmt.Sprintf("deleted all [entities.Message] for user with ID [%s]", userID)) @@ -126,7 +126,7 @@ func (service *MessageService) GetBulkMessages(ctx context.Context, userID entit orders, err := service.repository.GetBulkMessages(ctx, userID, 10) if err != nil { - return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "could not fetch bulk messages for user [%s]", userID)) + return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "could not fetch bulk messages for user [%s]", userID)) } ctxLogger.Info(fmt.Sprintf("fetched [%d] bulk messages for user [%s]", len(orders), userID)) @@ -141,7 +141,7 @@ func (service *MessageService) DeleteMessage(ctx context.Context, source string, ctxLogger := service.tracer.CtxLogger(service.logger, span) if err := service.repository.Delete(ctx, message.UserID, message.ID); err != nil { - return service.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCode(err, stacktrace.GetCode(err), "could not delete message with ID [%s] for user wit ID [%s]", message.ID, message.UserID)) + return service.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCodef(err, stacktrace.GetCode(err), "could not delete message with ID [%s] for user wit ID [%s]", message.ID, message.UserID)) } var prevID *uuid.UUID @@ -169,12 +169,12 @@ func (service *MessageService) DeleteMessage(ctx context.Context, source string, SIM: message.SIM, }) if err != nil { - return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot create [%T] for message with ID [%s]", event, message.ID)) + return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot create [%T] for message with ID [%s]", event, message.ID)) } ctxLogger.Info(fmt.Sprintf("created event [%s] with id [%s] for message [%s]", event.Type(), event.ID(), message.ID)) if err = service.eventDispatcher.Dispatch(ctx, event); err != nil { - return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot dispatch event [%s] with id [%s] for message [%s]", event.Type(), event.ID(), message.ID)) + return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot dispatch event [%s] with id [%s] for message [%s]", event.Type(), event.ID(), message.ID)) } ctxLogger.Info(fmt.Sprintf("dispatched event [%s] with id [%s] for message [%s]", event.Type(), event.ID(), message.ID)) @@ -189,7 +189,7 @@ func (service *MessageService) DeleteByOwnerAndContact(ctx context.Context, user ctxLogger := service.tracer.CtxLogger(service.logger, span) if err := service.repository.DeleteByOwnerAndContact(ctx, userID, owner, contact); err != nil { - return service.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCode(err, stacktrace.GetCode(err), "could not all delete messages for user with ID [%s] between owner [%s] and contact [%s] ", userID, owner, contact)) + return service.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCodef(err, stacktrace.GetCode(err), "could not all delete messages for user with ID [%s] between owner [%s] and contact [%s] ", userID, owner, contact)) } ctxLogger.Info(fmt.Sprintf("deleted all messages for user with ID [%s] between owner [%s] and contact [%s] ", userID, owner, contact)) @@ -203,7 +203,7 @@ func (service *MessageService) RespondToMissedCall(ctx context.Context, source s phone, err := service.phoneService.Load(ctx, payload.UserID, payload.Owner) if err != nil { - return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot find phone with owner [%s] for user with ID [%s] when handling missed phone call message [%s]", payload.Owner, payload.UserID, payload.MessageID)) + return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot find phone with owner [%s] for user with ID [%s] when handling missed phone call message [%s]", payload.Owner, payload.UserID, payload.MessageID)) } if phone.MissedCallAutoReply == nil || strings.TrimSpace(*phone.MissedCallAutoReply) == "" { @@ -225,7 +225,7 @@ func (service *MessageService) RespondToMissedCall(ctx context.Context, source s RequestReceivedAt: time.Now().UTC(), }) if err != nil { - return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot send auto response message for owner [%s] for user with ID [%s] when handling missed phone call message [%s]", payload.Owner, payload.UserID, payload.MessageID)) + return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot send auto response message for owner [%s] for user with ID [%s] when handling missed phone call message [%s]", payload.Owner, payload.UserID, payload.MessageID)) } ctxLogger.Info(fmt.Sprintf("created response message with ID [%s] for missed call event [%s] for user [%s]", message.ID, payload.MessageID, message.UserID)) @@ -249,7 +249,7 @@ func (service *MessageService) GetMessages(ctx context.Context, params MessageGe messages, err := service.repository.Index(ctx, params.UserID, params.Owner, params.Contact, params.IndexParams) if err != nil { - return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "could not fetch messages with parms [%+#v]", params)) + return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "could not fetch messages with parms [%+#v]", params)) } ctxLogger.Info(fmt.Sprintf("fetched [%d] messages with prams [%+#v]", len(*messages), params)) @@ -263,7 +263,7 @@ func (service *MessageService) GetMessage(ctx context.Context, userID entities.U message, err := service.repository.Load(ctx, userID, messageID) if err != nil { - return nil, service.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCode(err, stacktrace.GetCode(err), "could not fetch messages with ID [%s]", messageID)) + return nil, service.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCodef(err, stacktrace.GetCode(err), "could not fetch messages with ID [%s]", messageID)) } return message, nil @@ -293,11 +293,11 @@ func (service *MessageService) StoreEvent(ctx context.Context, message *entities case entities.MessageEventNameFailed: err = service.handleMessageFailedEvent(ctx, params, message) default: - return nil, service.tracer.WrapErrorSpan(span, stacktrace.NewError("cannot handle message event [%s]", params.EventName)) + return nil, service.tracer.WrapErrorSpan(span, stacktrace.NewErrorf("cannot handle message event [%s]", params.EventName)) } if err != nil { - return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "could not handle phone event [%s] for message with id [%s]", params.EventName, message.ID)) + return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "could not handle phone event [%s] for message with id [%s]", params.EventName, message.ID)) } return service.repository.Load(ctx, message.UserID, params.MessageID) @@ -328,7 +328,7 @@ func (service *MessageService) ReceiveMessage(ctx context.Context, params *Messa ctxLogger.Info(fmt.Sprintf("uploading [%d] attachments for user [%s] message [%s]", len(params.Attachments), params.UserID, messageID)) attachmentURLs, err := service.uploadAttachments(ctx, params.UserID, messageID, params.Attachments) if err != nil { - return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot upload attachments for user [%s] message [%s]", params.UserID, messageID)) + return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot upload attachments for user [%s] message [%s]", params.UserID, messageID)) } owner := phonenumbers.Format(params.Owner, phonenumbers.E164) @@ -349,13 +349,13 @@ func (service *MessageService) ReceiveMessage(ctx context.Context, params *Messa event, err := service.createMessagePhoneReceivedEvent(params.Source, eventPayload) if err != nil { - return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot create %T from payload with message id [%s]", event, eventPayload.MessageID)) + return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot create %T from payload with message id [%s]", event, eventPayload.MessageID)) } ctxLogger.Info(fmt.Sprintf("created event [%s] with id [%s] and message id [%s]", event.Type(), event.ID(), eventPayload.MessageID)) if err = service.eventDispatcher.Dispatch(ctx, event); err != nil { - return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot dispatch event type [%s] and id [%s]", event.Type(), event.ID())) + return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot dispatch event type [%s] and id [%s]", event.Type(), event.ID())) } ctxLogger.Info(fmt.Sprintf("event [%s] dispatched successfully", event.ID())) @@ -378,11 +378,11 @@ func (service *MessageService) handleMessageSentEvent(ctx context.Context, param SIM: message.SIM, }) if err != nil { - return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot create event [%s] for message [%s]", events.EventTypeMessagePhoneSent, message.ID)) + return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot create event [%s] for message [%s]", events.EventTypeMessagePhoneSent, message.ID)) } if err = service.eventDispatcher.Dispatch(ctx, event); err != nil { - return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot dispatch event type [%s] and id [%s]", event.Type(), event.ID())) + return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot dispatch event type [%s] and id [%s]", event.Type(), event.ID())) } return nil } @@ -403,11 +403,11 @@ func (service *MessageService) handleMessageDeliveredEvent(ctx context.Context, SIM: message.SIM, }) if err != nil { - return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot create event [%s] for message [%s]", events.EventTypeMessagePhoneSent, message.ID)) + return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot create event [%s] for message [%s]", events.EventTypeMessagePhoneSent, message.ID)) } if _, err = service.eventDispatcher.DispatchWithTimeout(ctx, event, time.Second); err != nil { - return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot dispatch event type [%s] and id [%s] for message [%s]", event.Type(), event.ID(), message.ID)) + return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot dispatch event type [%s] and id [%s] for message [%s]", event.Type(), event.ID(), message.ID)) } return nil } @@ -434,11 +434,11 @@ func (service *MessageService) handleMessageFailedEvent(ctx context.Context, par SIM: message.SIM, }) if err != nil { - return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot create event [%s] for message [%s]", events.EventTypeMessageSendFailed, message.ID)) + return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot create event [%s] for message [%s]", events.EventTypeMessageSendFailed, message.ID)) } if err = service.eventDispatcher.Dispatch(ctx, event); err != nil { - return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot dispatch event type [%s] and id [%s]", event.Type(), event.ID())) + return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot dispatch event type [%s] and id [%s]", event.Type(), event.ID())) } return nil } @@ -485,18 +485,18 @@ func (service *MessageService) SendMessage(ctx context.Context, params MessageSe event, err := service.createMessageAPISentEvent(params.Source, eventPayload) if err != nil { - return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot create %T from payload with message id [%s]", event, eventPayload.MessageID)) + return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot create %T from payload with message id [%s]", event, eventPayload.MessageID)) } ctxLogger.Info(fmt.Sprintf("created event [%s] with id [%s] and message id [%s] and user [%s]", event.Type(), event.ID(), eventPayload.MessageID, eventPayload.UserID)) message, err := service.storeSentMessage(ctx, eventPayload) if err != nil { - return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot store message with id [%s]", eventPayload.MessageID)) + return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot store message with id [%s]", eventPayload.MessageID)) } timeout := service.getSendDelay(ctxLogger, eventPayload, params, messagesPerMinute) if _, err = service.eventDispatcher.DispatchWithTimeout(ctx, event, timeout); err != nil { - return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot dispatch event type [%s] and id [%s]", event.Type(), event.ID())) + return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot dispatch event type [%s] and id [%s]", event.Type(), event.ID())) } ctxLogger.Info(fmt.Sprintf("[%s] event with ID [%s] dispatched succesfully for message [%s] with user [%s] and delay [%s]", event.Type(), event.ID(), eventPayload.MessageID, eventPayload.UserID, timeout)) @@ -531,18 +531,18 @@ func (service *MessageService) RegisterMissedCall(ctx context.Context, params *M event, err := service.createEvent(events.MessageCallMissed, params.Source, eventPayload) if err != nil { - return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot create [%T] from payload with message id [%s]", event, eventPayload.MessageID)) + return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot create [%T] from payload with message id [%s]", event, eventPayload.MessageID)) } ctxLogger.Info(fmt.Sprintf("created event [%s] with id [%s] and message id [%s] and user [%s]", event.Type(), event.ID(), eventPayload.MessageID, eventPayload.UserID)) message, err := service.storeMissedCallMessage(ctx, eventPayload) if err != nil { - return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot store missed call message message with id [%s]", eventPayload.MessageID)) + return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot store missed call message message with id [%s]", eventPayload.MessageID)) } if err = service.eventDispatcher.Dispatch(ctx, event); err != nil { - return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot dispatch event type [%s] and id [%s]", event.Type(), event.ID())) + return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot dispatch event type [%s] and id [%s]", event.Type(), event.ID())) } ctxLogger.Info(fmt.Sprintf("[%s] event with ID [%s] dispatched succesfully for message [%s] with user [%s]", event.Type(), event.ID(), eventPayload.MessageID, eventPayload.UserID)) @@ -595,7 +595,7 @@ func (service *MessageService) storeReceivedMessage(ctx context.Context, params } if err := service.repository.Store(ctx, message); err != nil { - return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot save message with id [%s]", params.MessageID)) + return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot save message with id [%s]", params.MessageID)) } ctxLogger.Info(fmt.Sprintf("message saved with id [%s]", message.ID)) @@ -619,7 +619,7 @@ func (service *MessageService) uploadAttachments(ctx context.Context, userID ent g.Go(func() error { decoded, err := base64.StdEncoding.DecodeString(attachment.Content) if err != nil { - return stacktrace.Propagate(err, "cannot decode base64 content for attachment [%d]", i) + return stacktrace.Propagatef(err, "cannot decode base64 content for attachment [%d]", i) } sanitizedName := repositories.SanitizeFilename(attachment.Name, i) @@ -630,7 +630,7 @@ func (service *MessageService) uploadAttachments(ctx context.Context, userID ent paths[i] = path if err = service.attachmentRepository.Upload(gCtx, path, decoded, attachment.ContentType); err != nil { - return stacktrace.Propagate(err, "cannot upload attachment [%d] to path [%s]", i, path) + return stacktrace.Propagatef(err, "cannot upload attachment [%d] to path [%s]", i, path) } urls[i] = fmt.Sprintf("%s/v1/attachments/%s/%s/%d/%s", service.apiBaseURL, userID, messageID, i, filename) @@ -645,7 +645,7 @@ func (service *MessageService) uploadAttachments(ctx context.Context, userID ent _ = service.attachmentRepository.Delete(ctx, path) } } - return nil, stacktrace.Propagate(err, "cannot upload attachments") + return nil, stacktrace.Propagatef(err, "cannot upload attachments") } return urls, nil @@ -668,15 +668,15 @@ func (service *MessageService) HandleMessageSending(ctx context.Context, params message, err := service.repository.Load(ctx, params.UserID, params.ID) if err != nil { - return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot find message with id [%s]", params.ID)) + return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot find message with id [%s]", params.ID)) } if !message.IsSending() { - return service.tracer.WrapErrorSpan(span, stacktrace.NewError("message has wrong status [%s]. expected %s", message.Status, entities.MessageStatusSending)) + return service.tracer.WrapErrorSpan(span, stacktrace.NewErrorf("message has wrong status [%s]. expected %s", message.Status, entities.MessageStatusSending)) } if err = service.repository.Update(ctx, message.AddSendAttempt(params.Timestamp)); err != nil { - return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot update message with id [%s] after sending", message.ID)) + return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot update message with id [%s] after sending", message.ID)) } ctxLogger.Info(fmt.Sprintf("message with id [%s] updated after adding send attempt", message.ID)) @@ -692,7 +692,7 @@ func (service *MessageService) HandleMessageSent(ctx context.Context, params Han message, err := service.repository.Load(ctx, params.UserID, params.ID) if err != nil { - return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot find message with id [%s]", params.ID)) + return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot find message with id [%s]", params.ID)) } if message.IsSent() || message.IsDelivered() { @@ -701,11 +701,11 @@ func (service *MessageService) HandleMessageSent(ctx context.Context, params Han } if !message.IsSending() && !message.IsExpired() && !message.IsScheduled() { - return service.tracer.WrapErrorSpan(span, stacktrace.NewError("message has wrong status [%s]. expected [%s, %s, %s]", message.Status, entities.MessageStatusSending, entities.MessageStatusExpired, entities.MessageStatusScheduled)) + return service.tracer.WrapErrorSpan(span, stacktrace.NewErrorf("message has wrong status [%s]. expected [%s, %s, %s]", message.Status, entities.MessageStatusSending, entities.MessageStatusExpired, entities.MessageStatusScheduled)) } if err = service.repository.Update(ctx, message.Sent(params.Timestamp)); err != nil { - return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot update message with id [%s] as sent", message.ID)) + return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot update message with id [%s] as sent", message.ID)) } ctxLogger.Info(fmt.Sprintf("message with id [%s] has been updated to status [%s]", message.ID, message.Status)) @@ -729,15 +729,15 @@ func (service *MessageService) HandleMessageFailed(ctx context.Context, params H message, err := service.repository.Load(ctx, params.UserID, params.ID) if err != nil { - return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot find message with id [%s]", params.ID)) + return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot find message with id [%s]", params.ID)) } if message.IsDelivered() { - return service.tracer.WrapErrorSpan(span, stacktrace.NewError("message has already been delivered with status [%s]", message.Status)) + return service.tracer.WrapErrorSpan(span, stacktrace.NewErrorf("message has already been delivered with status [%s]", message.Status)) } if err = service.repository.Update(ctx, message.Failed(params.Timestamp, params.ErrorMessage)); err != nil { - return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot update message with id [%s] as sent", message.ID)) + return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot update message with id [%s] as sent", message.ID)) } ctxLogger.Info(fmt.Sprintf("message with id [%s] has been updated to status [%s]", message.ID, message.Status)) @@ -753,16 +753,16 @@ func (service *MessageService) HandleMessageDelivered(ctx context.Context, param message, err := service.repository.Load(ctx, params.UserID, params.ID) if err != nil { - return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot find message with id [%s]", params.ID)) + return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot find message with id [%s]", params.ID)) } if !message.IsSent() && !message.IsSending() && !message.IsExpired() && !message.IsScheduled() { - ctxLogger.Warn(stacktrace.NewError("message has wrong status [%s]. expected [%s, %s, %s, %s]", message.Status, entities.MessageStatusSent, entities.MessageStatusScheduled, entities.MessageStatusSending, entities.MessageStatusExpired)) + ctxLogger.Warn(stacktrace.NewErrorf("message has wrong status [%s]. expected [%s, %s, %s, %s]", message.Status, entities.MessageStatusSent, entities.MessageStatusScheduled, entities.MessageStatusSending, entities.MessageStatusExpired)) return nil } if err = service.repository.Update(ctx, message.Delivered(params.Timestamp)); err != nil { - return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot update message with id [%s] as delivered", message.ID)) + return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot update message with id [%s] as delivered", message.ID)) } ctxLogger.Info(fmt.Sprintf("message with id [%s] has been updated to status [%s]", message.ID, message.Status)) @@ -778,15 +778,15 @@ func (service *MessageService) HandleMessageNotificationScheduled(ctx context.Co message, err := service.repository.Load(ctx, params.UserID, params.ID) if err != nil { - return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot find message with id [%s]", params.ID)) + return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot find message with id [%s]", params.ID)) } if !message.IsPending() && !message.IsExpired() && !message.IsSending() { - ctxLogger.Warn(stacktrace.NewError("received scheduled event for message with id [%s] message has status [%s]", message.ID, message.Status)) + ctxLogger.Warn(stacktrace.NewErrorf("received scheduled event for message with id [%s] message has status [%s]", message.ID, message.Status)) } if err = service.repository.Update(ctx, message.NotificationScheduled(params.Timestamp)); err != nil { - return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot update message with id [%s] as expired", message.ID)) + return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot update message with id [%s] as expired", message.ID)) } ctxLogger.Info(fmt.Sprintf("message with id [%s] has been scheduled to send at [%s]", message.ID, message.NotificationScheduledAt.String())) @@ -802,11 +802,11 @@ func (service *MessageService) HandleMessageNotificationSent(ctx context.Context message, err := service.repository.Load(ctx, params.UserID, params.ID) if err != nil { - return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot find message with id [%s]", params.ID)) + return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot find message with id [%s]", params.ID)) } if err = service.repository.Update(ctx, message.AddSendAttemptCount()); err != nil { - return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot update message with id [%s] as expired", message.ID)) + return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot update message with id [%s] as expired", message.ID)) } ctxLogger.Info(fmt.Sprintf("notification for message with id [%s] has been sent at [%s]", message.ID, params.Timestamp.String())) @@ -822,15 +822,15 @@ func (service *MessageService) HandleMessageExpired(ctx context.Context, params message, err := service.repository.Load(ctx, params.UserID, params.ID) if err != nil { - return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot find message with id [%s]", params.ID)) + return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot find message with id [%s]", params.ID)) } if !message.IsSending() && !message.IsScheduled() && !message.IsPending() { - return service.tracer.WrapErrorSpan(span, stacktrace.NewError("message has wrong status [%s]. expected [%s, %s, %s]", message.Status, entities.MessageStatusSending, entities.MessageStatusScheduled, entities.MessageStatusPending)) + return service.tracer.WrapErrorSpan(span, stacktrace.NewErrorf("message has wrong status [%s]. expected [%s, %s, %s]", message.Status, entities.MessageStatusSending, entities.MessageStatusScheduled, entities.MessageStatusPending)) } if err = service.repository.Update(ctx, message.Expired(params.Timestamp)); err != nil { - return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot update message with id [%s] as expired", message.ID)) + return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot update message with id [%s] as expired", message.ID)) } ctxLogger.Info(fmt.Sprintf("message with id [%s] has been updated to status [%s]", message.ID, message.Status)) @@ -850,11 +850,11 @@ func (service *MessageService) HandleMessageExpired(ctx context.Context, params SIM: message.SIM, }) if err != nil { - return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot create [%s] event for expired message with ID [%s]", events.EventTypeMessageSendRetry, message.ID)) + return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot create [%s] event for expired message with ID [%s]", events.EventTypeMessageSendRetry, message.ID)) } if err = service.eventDispatcher.Dispatch(ctx, event); err != nil { - return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot dispatch [%s] event for message with ID [%s]", event.Type(), message.ID)) + return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot dispatch [%s] event for message with ID [%s]", event.Type(), message.ID)) } ctxLogger.Info(fmt.Sprintf("retried sending message with ID [%s]", message.ID)) @@ -889,11 +889,11 @@ func (service *MessageService) ScheduleExpirationCheck(ctx context.Context, para UserID: params.UserID, }) if err != nil { - return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot create event [%s] for message with id [%s]", events.EventTypeMessageSendExpiredCheck, params.MessageID)) + return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot create event [%s] for message with id [%s]", events.EventTypeMessageSendExpiredCheck, params.MessageID)) } if _, err = service.eventDispatcher.DispatchWithTimeout(ctx, event, params.MessageExpirationDuration); err != nil { - return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot dispatch event [%s] for message with ID [%s]", event.Type(), params.MessageID)) + return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot dispatch event [%s] for message with ID [%s]", event.Type(), params.MessageID)) } ctxLogger.Info(fmt.Sprintf("scheduled message id [%s] to expire at [%s]", params.MessageID, params.NotificationSentAt.Add(params.MessageExpirationDuration))) @@ -921,7 +921,7 @@ func (service *MessageService) CheckExpired(ctx context.Context, params MessageC } if err != nil { - return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot load message with userID [%s] and messageID [%s]", params.UserID, params.MessageID)) + return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot load message with userID [%s] and messageID [%s]", params.UserID, params.MessageID)) } if !message.IsPending() && !message.IsSending() && !message.IsScheduled() { @@ -943,11 +943,11 @@ func (service *MessageService) CheckExpired(ctx context.Context, params MessageC SIM: message.SIM, }) if err != nil { - return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot create event [%s] for message with id [%s]", events.EventTypeMessageSendExpired, params.MessageID)) + return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot create event [%s] for message with id [%s]", events.EventTypeMessageSendExpired, params.MessageID)) } if err = service.eventDispatcher.Dispatch(ctx, event); err != nil { - return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot dispatch event [%s] for message with ID [%s]", event.Type(), params.MessageID)) + return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot dispatch event [%s] for message with ID [%s]", event.Type(), params.MessageID)) } ctxLogger.Info(fmt.Sprintf("message [%s] has expired with status [%s]", params.MessageID, message.Status)) @@ -972,7 +972,7 @@ func (service *MessageService) SearchMessages(ctx context.Context, params *Messa messages, err := service.repository.Search(ctx, params.UserID, params.Owners, params.Types, params.Statuses, params.IndexParams) if err != nil { - return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "could not search messages with parms [%+#v]", params)) + return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "could not search messages with parms [%+#v]", params)) } ctxLogger.Info(fmt.Sprintf("fetched [%d] messages with prams [%+#v]", len(messages), params)) @@ -987,7 +987,7 @@ func (service *MessageService) phoneSettings(ctx context.Context, userID entitie phone, err := service.phoneService.Load(ctx, userID, owner) if err != nil { - ctxLogger.Error(stacktrace.Propagate(err, "cannot load phone for userID [%s] and owner [%s]. using default max send attempt of 2", userID, owner)) + ctxLogger.Error(stacktrace.Propagatef(err, "cannot load phone for userID [%s] and owner [%s]. using default max send attempt of 2", userID, owner)) return 2, entities.SIM1, 0 } @@ -1027,7 +1027,7 @@ func (service *MessageService) storeSentMessage(ctx context.Context, payload eve } if err := service.repository.Store(ctx, message); err != nil { - return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot save message with id [%s]", payload.MessageID)) + return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot save message with id [%s]", payload.MessageID)) } ctxLogger.Info(fmt.Sprintf("message saved with id [%s]", payload.MessageID)) @@ -1056,7 +1056,7 @@ func (service *MessageService) storeMissedCallMessage(ctx context.Context, paylo } if err := service.repository.Store(ctx, message); err != nil { - return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot save missed call message with id [%s]", payload.MessageID)) + return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot save missed call message with id [%s]", payload.MessageID)) } ctxLogger.Info(fmt.Sprintf("missed call message saved with id [%s]", payload.MessageID)) diff --git a/api/pkg/services/message_thread_service.go b/api/pkg/services/message_thread_service.go index 687482ddd..532a223e2 100644 --- a/api/pkg/services/message_thread_service.go +++ b/api/pkg/services/message_thread_service.go @@ -70,7 +70,7 @@ func (service *MessageThreadService) DeleteAllForUser(ctx context.Context, userI defer span.End() if err := service.repository.DeleteAllForUser(ctx, userID); err != nil { - return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "could not delete [entities.MessageThread] for user with ID [%s]", userID)) + return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "could not delete [entities.MessageThread] for user with ID [%s]", userID)) } ctxLogger.Info(fmt.Sprintf("deleted all [entities.MessageThread] for user with ID [%s]", userID)) @@ -91,16 +91,16 @@ func (service *MessageThreadService) UpdateThread(ctx context.Context, params Me } if err != nil { - return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot find thread with owner [%s], and contact [%s]. creating new thread", params.Owner, params.Contact)) + return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot find thread with owner [%s], and contact [%s]. creating new thread", params.Owner, params.Contact)) } if thread.OrderTimestamp.Unix() > params.Timestamp.Unix() && thread.Status != entities.MessageStatusSending && thread.HasLastMessage(params.MessageID) { - ctxLogger.Warn(stacktrace.NewError("thread [%s] has timestamp [%s] and status [%s] which is greater than timestamp [%s] for message [%s] and status [%s]", thread.ID, thread.OrderTimestamp, thread.Status, params.Timestamp, params.MessageID, params.Status)) + ctxLogger.Warn(stacktrace.NewErrorf("thread [%s] has timestamp [%s] and status [%s] which is greater than timestamp [%s] for message [%s] and status [%s]", thread.ID, thread.OrderTimestamp, thread.Status, params.Timestamp, params.MessageID, params.Status)) return nil } if thread.Status == entities.MessageStatusDelivered && thread.LastMessageID != nil && thread.HasLastMessage(params.MessageID) { - ctxLogger.Warn(stacktrace.NewError("thread [%s] already has status [%s] not updating with status [%s] for message [%s]", thread.ID, thread.Status, params.Status, params.MessageID)) + ctxLogger.Warn(stacktrace.NewErrorf("thread [%s] already has status [%s] not updating with status [%s] for message [%s]", thread.ID, thread.Status, params.Status, params.MessageID)) return nil } @@ -118,7 +118,7 @@ func (service *MessageThreadService) UpdateThread(ctx context.Context, params Me if service.shouldCheckUnarchive(thread, params) { phone, phoneErr := service.phoneRepository.Load(ctx, params.UserID, params.Owner) if phoneErr != nil { - ctxLogger.Warn(stacktrace.Propagate(phoneErr, "cannot load phone [%s] for user [%s] to resolve UnarchiveThread; leaving thread [%s] archived", params.Owner, params.UserID, thread.ID)) + ctxLogger.Warn(stacktrace.Propagatef(phoneErr, "cannot load phone [%s] for user [%s] to resolve UnarchiveThread; leaving thread [%s] archived", params.Owner, params.UserID, thread.ID)) } else if phone.UnarchiveThread { activity.Unarchive = true ctxLogger.Info(fmt.Sprintf("unarchiving thread [%s] after inbound message [%s]", thread.ID, params.MessageID)) @@ -126,7 +126,7 @@ func (service *MessageThreadService) UpdateThread(ctx context.Context, params Me } if err = service.repository.UpdateActivity(ctx, activity); err != nil { - return service.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCode(err, stacktrace.GetCode(err), "cannot update message thread with id [%s] after adding message [%s]", thread.ID, params.MessageID)) + return service.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCodef(err, stacktrace.GetCode(err), "cannot update message thread with id [%s] after adding message [%s]", thread.ID, params.MessageID)) } ctxLogger.Info(fmt.Sprintf("thread with id [%s] updated with last message [%s] and status [%s]", thread.ID, params.MessageID, params.Status)) @@ -153,7 +153,7 @@ func (service *MessageThreadService) UpdateStatus(ctx context.Context, params Me } thread, err := service.repository.UpdateStatus(ctx, params.UserID, params.MessageThreadID, update) if err != nil { - return nil, service.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCode(err, stacktrace.GetCode(err), "cannot update message thread with ID [%s] for user [%s]", params.MessageThreadID, params.UserID)) + return nil, service.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCodef(err, stacktrace.GetCode(err), "cannot update message thread with ID [%s] for user [%s]", params.MessageThreadID, params.UserID)) } return thread, nil @@ -166,12 +166,12 @@ func (service *MessageThreadService) UpdateAfterDeletedMessage(ctx context.Conte thread, err := service.repository.LoadByOwnerContact(ctx, payload.UserID, payload.Owner, payload.Contact) if err != nil { - return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot find thread for user [%s] with owner [%s], and contact [%s]", payload.UserID, payload.Owner, payload.Contact)) + return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot find thread for user [%s] with owner [%s], and contact [%s]", payload.UserID, payload.Owner, payload.Contact)) } if payload.PreviousMessageID == nil { if err = service.repository.Delete(ctx, thread.UserID, thread.ID); err != nil { - ctxLogger.Error(stacktrace.Propagate(err, "cannot delete thread with ID [%s] for user [%s] and owner [%s]", thread.ID, thread.UserID, thread.Owner)) + ctxLogger.Error(stacktrace.Propagatef(err, "cannot delete thread with ID [%s] for user [%s] and owner [%s]", thread.ID, thread.UserID, thread.Owner)) return nil } msg := fmt.Sprintf("previous message ID is nil for thread with ID [%s] and user [%s]", thread.ID, thread.UserID) @@ -192,7 +192,7 @@ func (service *MessageThreadService) UpdateAfterDeletedMessage(ctx context.Conte LastMessageContent: payload.PreviousMessageContent, LastMessageStatus: *payload.PreviousMessageStatus, }); err != nil { - return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot update thread with ID [%s] for user with ID [%s]", thread.ID, thread.UserID)) + return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot update thread with ID [%s] for user with ID [%s]", thread.ID, thread.UserID)) } ctxLogger.Info(fmt.Sprintf("last message has been removed from thread with ID [%s] and userID [%s]", thread.ID, thread.UserID)) @@ -224,7 +224,7 @@ func (service *MessageThreadService) createThread(ctx context.Context, params Me } if err := service.repository.Store(ctx, thread); err != nil { - return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot store thread with id [%s] for message with ID [%s]", thread.ID, params.MessageID)) + return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot store thread with id [%s] for message with ID [%s]", thread.ID, params.MessageID)) } ctxLogger.Info(fmt.Sprintf( @@ -279,7 +279,7 @@ func (service *MessageThreadService) GetThreads(ctx context.Context, params Mess threads, err := service.repository.Index(ctx, params.UserID, params.Owner, params.IsArchived, params.IndexParams) if err != nil { - return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "could not fetch messages threads for params [%+#v]", params)) + return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "could not fetch messages threads for params [%+#v]", params)) } ctxLogger.Info(fmt.Sprintf("fetched [%d] threads with params [%+#v]", len(*threads), params)) @@ -293,7 +293,7 @@ func (service *MessageThreadService) GetThread(ctx context.Context, userID entit thread, err := service.repository.Load(ctx, userID, messageThreadID) if err != nil { - return nil, service.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCode(err, stacktrace.GetCode(err), "could not fetch thread with ID [%s] for user [%s]", messageThreadID, userID)) + return nil, service.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCodef(err, stacktrace.GetCode(err), "could not fetch thread with ID [%s] for user [%s]", messageThreadID, userID)) } return thread, nil @@ -305,7 +305,7 @@ func (service *MessageThreadService) DeleteThread(ctx context.Context, source st defer span.End() if err := service.repository.Delete(ctx, thread.UserID, thread.ID); err != nil { - return service.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCode(err, stacktrace.GetCode(err), "could not delete message thread with ID [%s] for user with ID [%s]", thread.ID, thread.UserID)) + return service.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCodef(err, stacktrace.GetCode(err), "could not delete message thread with ID [%s] for user with ID [%s]", thread.ID, thread.UserID)) } event, err := service.createEvent(events.MessageThreadAPIDeleted, source, &events.MessageThreadAPIDeletedPayload{ @@ -319,12 +319,12 @@ func (service *MessageThreadService) DeleteThread(ctx context.Context, source st Timestamp: time.Now().UTC(), }) if err != nil { - return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot create [%T] for message thread dleted with ID [%s]", event, thread.ID)) + return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot create [%T] for message thread dleted with ID [%s]", event, thread.ID)) } ctxLogger.Info(fmt.Sprintf("created event [%s] with id [%s] for message thread [%s]", event.Type(), event.ID(), thread.ID)) if err = service.eventDispatcher.Dispatch(ctx, event); err != nil { - return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot dispatch event [%s] with id [%s] for message thread [%s]", event.Type(), event.ID(), thread.ID)) + return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot dispatch event [%s] with id [%s] for message thread [%s]", event.Type(), event.ID(), thread.ID)) } ctxLogger.Info(fmt.Sprintf("dispatched [%s] event with id [%s] for message thread [%s]", event.Type(), event.ID(), thread.ID)) diff --git a/api/pkg/services/message_thread_service_test.go b/api/pkg/services/message_thread_service_test.go index 7825be755..8cc8bdcbe 100644 --- a/api/pkg/services/message_thread_service_test.go +++ b/api/pkg/services/message_thread_service_test.go @@ -149,7 +149,7 @@ func TestCreateThreadSetsReadStateFromActivityDirection(t *testing.T) { var stored *entities.MessageThread repository := &messageThreadRepositoryStub{ loadByOwnerContact: func(context.Context, entities.UserID, string, string) (*entities.MessageThread, error) { - return nil, stacktrace.PropagateWithCode(gorm.ErrRecordNotFound, repositories.ErrCodeNotFound, "not found") + return nil, stacktrace.PropagateWithCodef(gorm.ErrRecordNotFound, repositories.ErrCodeNotFound, "not found") }, store: func(_ context.Context, thread *entities.MessageThread) error { stored = thread @@ -206,7 +206,7 @@ func TestUpdateStatusChangesOnlyRequestedState(t *testing.T) { func TestUpdateStatusPreservesNotFoundCode(t *testing.T) { repository := &messageThreadRepositoryStub{ updateStatus: func(context.Context, entities.UserID, uuid.UUID, repositories.MessageThreadStatusUpdate) (*entities.MessageThread, error) { - return nil, stacktrace.PropagateWithCode(gorm.ErrRecordNotFound, repositories.ErrCodeNotFound, "not found") + return nil, stacktrace.PropagateWithCodef(gorm.ErrRecordNotFound, repositories.ErrCodeNotFound, "not found") }, } diff --git a/api/pkg/services/phone_api_key_service.go b/api/pkg/services/phone_api_key_service.go index 9762f116e..83e9e696a 100644 --- a/api/pkg/services/phone_api_key_service.go +++ b/api/pkg/services/phone_api_key_service.go @@ -55,7 +55,7 @@ func (service *PhoneAPIKeyService) Index(ctx context.Context, userID entities.Us phoneAPIKeys, err := service.repository.Index(ctx, userID, params) if err != nil { - return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "could not fetch phone API Keys with params [%+#v]", params)) + return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "could not fetch phone API Keys with params [%+#v]", params)) } ctxLogger.Info(fmt.Sprintf("fetched [%d] phone API Keys with prams [%+#v]", len(phoneAPIKeys), params)) @@ -69,7 +69,7 @@ func (service *PhoneAPIKeyService) Create(ctx context.Context, authContext entit apiKey, err := service.generateAPIKey(64) if err != nil { - return nil, stacktrace.Propagate(err, "cannot generate API key") + return nil, stacktrace.Propagatef(err, "cannot generate API key") } phoneAPIKey := &entities.PhoneAPIKey{ @@ -85,7 +85,7 @@ func (service *PhoneAPIKeyService) Create(ctx context.Context, authContext entit } if err = service.repository.Create(ctx, phoneAPIKey); err != nil { - return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot create PhoneAPIKey for user [%s]", authContext.ID)) + return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot create PhoneAPIKey for user [%s]", authContext.ID)) } ctxLogger.Info(fmt.Sprintf("created [%T] with ID [%s] for user ID [%s]", phoneAPIKey, phoneAPIKey.ID, authContext.ID)) @@ -99,11 +99,11 @@ func (service *PhoneAPIKeyService) Delete(ctx context.Context, userID entities.U phoneAPIKey, err := service.repository.Load(ctx, userID, phoneAPIKeyID) if err != nil { - return stacktrace.Propagate(err, "cannot load [%T] with ID [%s] for user [%s]", &entities.PhoneAPIKey{}, phoneAPIKeyID, userID.String()) + return stacktrace.Propagatef(err, "cannot load [%T] with ID [%s] for user [%s]", &entities.PhoneAPIKey{}, phoneAPIKeyID, userID.String()) } if err = service.repository.Delete(ctx, phoneAPIKey); err != nil { - return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot delete [%T] with ID [%s] for user [%s]", phoneAPIKey, phoneAPIKey.ID, phoneAPIKey.UserID)) + return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot delete [%T] with ID [%s] for user [%s]", phoneAPIKey, phoneAPIKey.ID, phoneAPIKey.UserID)) } ctxLogger.Info(fmt.Sprintf("deleted [%T] with ID [%s] for user ID [%s]", phoneAPIKey, phoneAPIKey.ID, userID)) @@ -117,16 +117,16 @@ func (service *PhoneAPIKeyService) RemovePhone(ctx context.Context, userID entit phone, err := service.phoneRepository.LoadByID(ctx, userID, phoneID) if err != nil { - return stacktrace.Propagate(err, "cannot load [%T] with ID [%s] for user [%s]", &entities.Phone{}, phoneID, userID.String()) + return stacktrace.Propagatef(err, "cannot load [%T] with ID [%s] for user [%s]", &entities.Phone{}, phoneID, userID.String()) } phoneAPIKey, err := service.repository.Load(ctx, userID, phoneAPIKeyID) if err != nil { - return stacktrace.Propagate(err, "cannot load [%T] with ID [%s] for user [%s]", &entities.PhoneAPIKey{}, phoneAPIKeyID, userID.String()) + return stacktrace.Propagatef(err, "cannot load [%T] with ID [%s] for user [%s]", &entities.PhoneAPIKey{}, phoneAPIKeyID, userID.String()) } if err = service.repository.RemovePhone(ctx, phoneAPIKey, phone); err != nil { - return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot remove [%T] with ID [%s] from phone API key with ID [%s] for user [%s]", phone, phone.ID, phoneAPIKey.ID, userID)) + return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot remove [%T] with ID [%s] from phone API key with ID [%s] for user [%s]", phone, phone.ID, phoneAPIKey.ID, userID)) } ctxLogger.Info(fmt.Sprintf("removed [%T] with ID [%s] from [%T] with ID [%s] for user ID [%s]", phone, phoneID, phoneAPIKey, phoneAPIKeyID, userID)) @@ -139,7 +139,7 @@ func (service *PhoneAPIKeyService) RemovePhoneByID(ctx context.Context, userID e defer span.End() if err := service.repository.RemovePhoneByID(ctx, userID, phoneID, phoneNumber); err != nil { - return stacktrace.Propagate(err, "cannot remove [%T] with ID [%s] and number [%s] for user [%s]", &entities.Phone{}, phoneID, phoneNumber, userID.String()) + return stacktrace.Propagatef(err, "cannot remove [%T] with ID [%s] and number [%s] for user [%s]", &entities.Phone{}, phoneID, phoneNumber, userID.String()) } ctxLogger.Info(fmt.Sprintf("removed phone with ID [%s] from [%T] for user ID [%s]", phoneID, &entities.PhoneAPIKey{}, userID)) @@ -152,7 +152,7 @@ func (service *PhoneAPIKeyService) DeleteAllForUser(ctx context.Context, userID defer span.End() if err := service.repository.DeleteAllForUser(ctx, userID); err != nil { - return stacktrace.Propagate(err, "cannot delete all [%T] for user ID [%s]", &entities.PhoneAPIKey{}, userID) + return stacktrace.Propagatef(err, "cannot delete all [%T] for user ID [%s]", &entities.PhoneAPIKey{}, userID) } ctxLogger.Info(fmt.Sprintf("deleted all [%T] for user ID [%s]", &entities.PhoneAPIKey{}, userID)) @@ -166,16 +166,16 @@ func (service *PhoneAPIKeyService) AddPhone(ctx context.Context, userID entities phone, err := service.phoneRepository.LoadByID(ctx, userID, phoneID) if err != nil { - return stacktrace.Propagate(err, "cannot load [%T] with ID [%s] for user [%s]", &entities.Phone{}, phoneID, userID.String()) + return stacktrace.Propagatef(err, "cannot load [%T] with ID [%s] for user [%s]", &entities.Phone{}, phoneID, userID.String()) } phoneAPIKey, err := service.repository.Load(ctx, userID, phoneAPIKeyID) if err != nil { - return stacktrace.Propagate(err, "cannot load [%T] with ID [%s] for user [%s]", &entities.PhoneAPIKey{}, phoneAPIKeyID, userID.String()) + return stacktrace.Propagatef(err, "cannot load [%T] with ID [%s] for user [%s]", &entities.PhoneAPIKey{}, phoneAPIKeyID, userID.String()) } if err = service.repository.AddPhone(ctx, phoneAPIKey, phone); err != nil { - return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot add [%T] with ID [%s] to phone API key with ID [%s] for user [%s]", phone, phone.ID, phoneAPIKey.ID, userID)) + return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot add [%T] with ID [%s] to phone API key with ID [%s] for user [%s]", phone, phone.ID, phoneAPIKey.ID, userID)) } ctxLogger.Info(fmt.Sprintf("added [%T] with ID [%s] to [%T] with ID [%s] for user ID [%s]", phone, phone.ID, phoneAPIKey, phoneAPIKeyID, userID)) @@ -186,7 +186,7 @@ func (service *PhoneAPIKeyService) generateRandomBytes(n int) ([]byte, error) { b := make([]byte, n) // Note that err == nil only if we read len(b) bytes. if _, err := rand.Read(b); err != nil { - return nil, stacktrace.Propagate(err, "cannot generate [%d] random bytes", n) + return nil, stacktrace.Propagatef(err, "cannot generate [%d] random bytes", n) } return b, nil @@ -194,5 +194,5 @@ func (service *PhoneAPIKeyService) generateRandomBytes(n int) ([]byte, error) { func (service *PhoneAPIKeyService) generateAPIKey(n int) (string, error) { b, err := service.generateRandomBytes(n) - return base64.URLEncoding.EncodeToString(b)[0:n], stacktrace.Propagate(err, "cannot generate [%d] random bytes", n) + return base64.URLEncoding.EncodeToString(b)[0:n], stacktrace.Propagatef(err, "cannot generate [%d] random bytes", n) } diff --git a/api/pkg/services/phone_notification_service.go b/api/pkg/services/phone_notification_service.go index aed806ef1..0d4d8d209 100644 --- a/api/pkg/services/phone_notification_service.go +++ b/api/pkg/services/phone_notification_service.go @@ -57,7 +57,7 @@ func (service *PhoneNotificationService) DeleteAllForUser(ctx context.Context, u defer span.End() if err := service.phoneNotificationRepository.DeleteAllForUser(ctx, userID); err != nil { - return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "could not delete all [entities.PhoneNotification] for user with ID [%s]", userID)) + return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "could not delete all [entities.PhoneNotification] for user with ID [%s]", userID)) } ctxLogger.Info(fmt.Sprintf("deleted all [entities.PhoneNotification] for user with ID [%s]", userID)) @@ -70,7 +70,7 @@ func (service *PhoneNotificationService) DeleteByMessageID(ctx context.Context, defer span.End() if err := service.phoneNotificationRepository.DeleteByMessageID(ctx, userID, messageID); err != nil { - return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "could not delete [entities.PhoneNotification] for user [%s] and message with ID [%s]", userID, messageID)) + return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "could not delete [entities.PhoneNotification] for user [%s] and message with ID [%s]", userID, messageID)) } ctxLogger.Info(fmt.Sprintf("deleted [entities.PhoneNotification] for user [%s] and message with ID [%s]", userID, messageID)) @@ -84,11 +84,11 @@ func (service *PhoneNotificationService) SendHeartbeatFCM(ctx context.Context, p phone, err := service.phoneRepository.LoadByID(ctx, payload.UserID, payload.PhoneID) if err != nil { - return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot load phone with userID [%s] and phoneID [%s]", payload.UserID, payload.PhoneID)) + return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot load phone with userID [%s] and phoneID [%s]", payload.UserID, payload.PhoneID)) } if phone.FcmToken == nil { - return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "phone with id [%s] has no FCM token", phone.ID)) + return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "phone with id [%s] has no FCM token", phone.ID)) } result, err := service.messagingClient.Send(ctx, &messaging.Message{ @@ -101,7 +101,7 @@ func (service *PhoneNotificationService) SendHeartbeatFCM(ctx context.Context, p Token: *phone.FcmToken, }) if err != nil { - ctxLogger.Warn(stacktrace.Propagate(err, "cannot send heartbeat FCM to phone with id [%s] for user [%s]", phone.ID, phone.UserID)) + ctxLogger.Warn(stacktrace.Propagatef(err, "cannot send heartbeat FCM to phone with id [%s] for user [%s]", phone.ID, phone.UserID)) return nil } @@ -153,7 +153,7 @@ func (service *PhoneNotificationService) Send(ctx context.Context, params *Phone Token: *phone.FcmToken, }) if err != nil { - ctxLogger.Warn(stacktrace.Propagate( + ctxLogger.Warn(stacktrace.Propagatef( err, "cannot send FCM to phone with ID [%s] for user with ID [%s] and message [%s]", @@ -191,7 +191,7 @@ func (service *PhoneNotificationService) Schedule(ctx context.Context, params *P phone, err := service.phoneRepository.Load(ctx, params.UserID, params.Owner) if err != nil { - return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot load phone with userID [%s] and phone [%s]", params.UserID, params.Owner)) + return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot load phone with userID [%s] and phone [%s]", params.UserID, params.Owner)) } notification := &entities.PhoneNotification{ @@ -213,7 +213,7 @@ func (service *PhoneNotificationService) Schedule(ctx context.Context, params *P if phone.MessageSendScheduleID != nil { schedule, err = service.messageSendScheduleRepository.Load(ctx, params.UserID, *phone.MessageSendScheduleID) if err != nil && stacktrace.GetCode(err) != repositories.ErrCodeNotFound { - return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot load send schedule [%s] for phone [%s]", *phone.MessageSendScheduleID, phone.ID)) + return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot load send schedule [%s] for phone [%s]", *phone.MessageSendScheduleID, phone.ID)) } } @@ -222,7 +222,7 @@ func (service *PhoneNotificationService) Schedule(ctx context.Context, params *P } if err = service.phoneNotificationRepository.Schedule(ctx, phone.MessagesPerMinute, schedule, notification); err != nil { - return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot schedule notification for message [%s] to phone [%s]", params.MessageID, phone.ID)) + return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot schedule notification for message [%s] to phone [%s]", params.MessageID, phone.ID)) } if err = service.dispatchMessageNotificationScheduled(ctx, params, notification); err != nil { @@ -258,7 +258,7 @@ func (service *PhoneNotificationService) scheduleExact( notification.ScheduledAt = scheduledAt if err := service.phoneNotificationRepository.ScheduleExact(ctx, notification); err != nil { - return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot schedule exact notification for message [%s] to phone [%s]", params.MessageID, phone.ID)) + return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot schedule exact notification for message [%s] to phone [%s]", params.MessageID, phone.ID)) } if err := service.dispatchMessageNotificationScheduled(ctx, params, notification); err != nil { @@ -291,11 +291,11 @@ func (service *PhoneNotificationService) dispatchMessageNotificationSend( NotificationID: notification.ID, }) if err != nil { - return stacktrace.Propagate(err, "cannot create [%s] event for notification [%s]", events.EventTypeMessageNotificationSend, notification.ID) + return stacktrace.Propagatef(err, "cannot create [%s] event for notification [%s]", events.EventTypeMessageNotificationSend, notification.ID) } if _, err = service.eventDispatcher.DispatchWithTimeout(ctx, event, notification.ScheduledAt.Sub(time.Now())); err != nil { - return stacktrace.Propagate(err, "cannot dispatch event [%s] for notification [%s]", event.Type(), notification.ID) + return stacktrace.Propagatef(err, "cannot dispatch event [%s] for notification [%s]", event.Type(), notification.ID) } return nil } @@ -318,11 +318,11 @@ func (service *PhoneNotificationService) dispatchMessageNotificationScheduled( NotificationID: notification.ID, }) if err != nil { - return stacktrace.Propagate(err, "cannot create [%s] event for notification [%s]", events.EventTypeMessageNotificationScheduled, notification.ID) + return stacktrace.Propagatef(err, "cannot create [%s] event for notification [%s]", events.EventTypeMessageNotificationScheduled, notification.ID) } if err = service.eventDispatcher.Dispatch(ctx, event); err != nil { - return stacktrace.Propagate(err, "cannot dispatch event [%s] for notification [%s]", event.Type(), notification.ID) + return stacktrace.Propagatef(err, "cannot dispatch event [%s] for notification [%s]", event.Type(), notification.ID) } return nil } @@ -333,15 +333,15 @@ func (service *PhoneNotificationService) handleNotificationFailed(ctx context.Co ctxLogger := service.tracer.CtxLogger(service.logger, span) - ctxLogger.Warn(stacktrace.Propagate(err, "cannot send notification for message [%s] to phone [%s]", params.MessageID, params.PhoneNotificationID)) + ctxLogger.Warn(stacktrace.Propagatef(err, "cannot send notification for message [%s] to phone [%s]", params.MessageID, params.PhoneNotificationID)) event, err := service.createMessageNotificationFailedEvent(params.Source, err.Error(), params) if err != nil { - return stacktrace.Propagate(err, "cannot create [%s] event for notification [%s]", events.EventTypeMessageNotificationFailed, params.PhoneNotificationID) + return stacktrace.Propagatef(err, "cannot create [%s] event for notification [%s]", events.EventTypeMessageNotificationFailed, params.PhoneNotificationID) } if err = service.eventDispatcher.Dispatch(ctx, event); err != nil { - return stacktrace.Propagate(err, "cannot dispatch event [%s] for notification [%s]", event.Type(), params.PhoneNotificationID) + return stacktrace.Propagatef(err, "cannot dispatch event [%s] for notification [%s]", event.Type(), params.PhoneNotificationID) } service.updateStatus(ctx, params.PhoneNotificationID, entities.PhoneNotificationStatusFailed) @@ -363,11 +363,11 @@ func (service *PhoneNotificationService) handleNotificationSent( event, err := service.createMessageNotificationSentEvent(params.Source, phone, result, params) if err != nil { - return stacktrace.Propagate(err, "cannot create [%s] event for notification [%s]", events.EventTypeMessageNotificationSent, params.PhoneNotificationID) + return stacktrace.Propagatef(err, "cannot create [%s] event for notification [%s]", events.EventTypeMessageNotificationSent, params.PhoneNotificationID) } if err = service.eventDispatcher.Dispatch(ctx, event); err != nil { - return stacktrace.Propagate(err, "cannot dispatch event [%s] for notification [%s]", event.Type(), params.PhoneNotificationID) + return stacktrace.Propagatef(err, "cannot dispatch event [%s] for notification [%s]", event.Type(), params.PhoneNotificationID) } service.updateStatus(ctx, params.PhoneNotificationID, entities.PhoneNotificationStatusSent) @@ -413,7 +413,7 @@ func (service *PhoneNotificationService) createMessageNotificationSentEvent( } if err := event.SetData(cloudevents.ApplicationJSON, payload); err != nil { - return event, stacktrace.Propagate(err, "cannot encode %T [%#+v] as JSON", payload, payload) + return event, stacktrace.Propagatef(err, "cannot encode %T [%#+v] as JSON", payload, payload) } return event, nil @@ -441,7 +441,7 @@ func (service *PhoneNotificationService) createMessageNotificationFailedEvent( } if err := event.SetData(cloudevents.ApplicationJSON, payload); err != nil { - return event, stacktrace.Propagate(err, "cannot encode %T [%#+v] as JSON", payload, payload) + return event, stacktrace.Propagatef(err, "cannot encode %T [%#+v] as JSON", payload, payload) } return event, nil @@ -459,7 +459,7 @@ func (service *PhoneNotificationService) updateStatus( err := service.phoneNotificationRepository.UpdateStatus(ctx, notificationID, status) if err != nil { - ctxLogger.Error(stacktrace.Propagate(err, "cannot update status of notification with id [%s] to [%s]", notificationID, status)) + ctxLogger.Error(stacktrace.Propagatef(err, "cannot update status of notification with id [%s] to [%s]", notificationID, status)) } ctxLogger.Info(fmt.Sprintf("updated status of notification with id [%s] to [%s]", notificationID, status)) diff --git a/api/pkg/services/phone_service.go b/api/pkg/services/phone_service.go index c61bd1210..c7078375b 100644 --- a/api/pkg/services/phone_service.go +++ b/api/pkg/services/phone_service.go @@ -48,7 +48,7 @@ func (service *PhoneService) DeleteAllForUser(ctx context.Context, userID entiti defer span.End() if err := service.repository.DeleteAllForUser(ctx, userID); err != nil { - return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "could not delete all [entities.Phone] for user with ID [%s]", userID)) + return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "could not delete all [entities.Phone] for user with ID [%s]", userID)) } ctxLogger.Info(fmt.Sprintf("deleted all [entities.Phone] for user with ID [%s]", userID)) @@ -61,7 +61,7 @@ func (service *PhoneService) NullifyScheduleID(ctx context.Context, userID entit defer span.End() if err := service.repository.NullifyScheduleID(ctx, userID, scheduleID); err != nil { - return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot nullify schedule ID [%s] for user [%s]", scheduleID, userID)) + return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot nullify schedule ID [%s] for user [%s]", scheduleID, userID)) } service.tracer.CtxLogger(service.logger, span).Info(fmt.Sprintf("nullified schedule ID [%s] on phones for user [%s]", scheduleID, userID)) @@ -77,7 +77,7 @@ func (service *PhoneService) Index(ctx context.Context, authUser entities.AuthCo phones, err := service.repository.Index(ctx, authUser.ID, params) if err != nil { - return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "could not fetch phones with parms [%+#v]", params)) + return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "could not fetch phones with parms [%+#v]", params)) } ctxLogger.Info(fmt.Sprintf("fetched [%d] phones with prams [%+#v]", len(*phones), params)) @@ -129,11 +129,11 @@ func (service *PhoneService) Upsert(ctx context.Context, params *PhoneUpsertPara } if err != nil { - return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot upsert phone with id [%s] and number [%s]", phone.ID, phone.PhoneNumber)) + return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot upsert phone with id [%s] and number [%s]", phone.ID, phone.PhoneNumber)) } if err = service.repository.Save(ctx, service.update(phone, params)); err != nil { - return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot update phone with id [%s] and number [%s]", phone.ID, phone.PhoneNumber)) + return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot update phone with id [%s] and number [%s]", phone.ID, phone.PhoneNumber)) } ctxLogger.Info(fmt.Sprintf("phone updated with id [%s] in the phone repository for user [%s]", phone.ID, phone.UserID)) @@ -161,11 +161,11 @@ func (service *PhoneService) dispatchPhoneUpdatedEvent(ctx context.Context, phon SIM: phone.SIM, }) if err != nil { - return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot create event when phone [%s] is updated for user [%s]", phone.ID, phone.UserID)) + return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot create event when phone [%s] is updated for user [%s]", phone.ID, phone.UserID)) } if err = service.dispatcher.Dispatch(ctx, event); err != nil { - return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot dispatch event [%s] for phone with id [%s]", event.Type(), phone.ID)) + return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot dispatch event [%s] for phone with id [%s]", event.Type(), phone.ID)) } return nil } @@ -179,11 +179,11 @@ func (service *PhoneService) Delete(ctx context.Context, source string, userID e phone, err := service.repository.LoadByID(ctx, userID, phoneID) if err != nil { - return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot load phone with userID [%s] and phoneID [%s]", userID, phoneID)) + return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot load phone with userID [%s] and phoneID [%s]", userID, phoneID)) } if err = service.repository.Delete(ctx, userID, phoneID); err != nil { - return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot delete phone with id [%s] and user id [%s]", phoneID, userID)) + return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot delete phone with id [%s] and user id [%s]", phoneID, userID)) } ctxLogger.Info(fmt.Sprintf("deleted phone with id [%s] and user id [%s]", phoneID, userID)) @@ -196,11 +196,11 @@ func (service *PhoneService) Delete(ctx context.Context, source string, userID e SIM: phone.SIM, }) if err != nil { - return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot create event when phone is deleted")) + return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot create event when phone is deleted")) } if err = service.dispatcher.Dispatch(ctx, event); err != nil { - return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot dispatch event [%s] for phone with id [%s]", event.Type(), phone.ID)) + return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot dispatch event [%s] for phone with id [%s]", event.Type(), phone.ID)) } return nil @@ -228,14 +228,14 @@ func (service *PhoneService) UpsertFCMToken(ctx context.Context, params *PhoneFC } if err != nil { - return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot upsert FCM token for user with id [%s] and number [%s]", params.UserID, params.PhoneNumber)) + return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot upsert FCM token for user with id [%s] and number [%s]", params.UserID, params.PhoneNumber)) } phone.FcmToken = params.FcmToken phone.SIM = params.SIM if err = service.repository.Save(ctx, phone); err != nil { - return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot update phone with id [%s] and number [%s]", phone.ID, phone.PhoneNumber)) + return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot update phone with id [%s] and number [%s]", phone.ID, phone.PhoneNumber)) } ctxLogger.Info(fmt.Sprintf("phone updated with id [%s] in the phone repository for user [%s]", phone.ID, phone.UserID)) @@ -264,7 +264,7 @@ func (service *PhoneService) createPhone(ctx context.Context, params *PhoneFCMTo } if err := service.repository.Save(ctx, phone); err != nil { - return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot create phone with id [%s] and number [%s]", phone.ID, phone.PhoneNumber)) + return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot create phone with id [%s] and number [%s]", phone.ID, phone.PhoneNumber)) } ctxLogger.Info(fmt.Sprintf("phone updated with id [%s] in the phone repository for user [%s]", phone.ID, phone.UserID)) diff --git a/api/pkg/services/service.go b/api/pkg/services/service.go index 605921fa6..2705bec2d 100644 --- a/api/pkg/services/service.go +++ b/api/pkg/services/service.go @@ -23,7 +23,7 @@ func (service *service) createEvent(eventType string, source string, payload any event.SetID(uuid.New().String()) if err := event.SetData(cloudevents.ApplicationJSON, payload); err != nil { - return event, stacktrace.Propagate(err, "cannot encode %T [%#+v] as JSON", payload, payload) + return event, stacktrace.Propagatef(err, "cannot encode %T [%#+v] as JSON", payload, payload) } return event, nil @@ -34,7 +34,7 @@ func (service *service) getFormattedNumber(ctxLogger telemetry.Logger, phoneNumb matched, err := regexp.MatchString(phoneNumberPattern, phoneNumber) if err != nil { - ctxLogger.Error(stacktrace.Propagate(err, "error while matching phoneNumber [%s] with regex [%s]", phoneNumber, phoneNumberPattern)) + ctxLogger.Error(stacktrace.Propagatef(err, "error while matching phoneNumber [%s] with regex [%s]", phoneNumber, phoneNumberPattern)) return phoneNumber } if !matched { @@ -43,7 +43,7 @@ func (service *service) getFormattedNumber(ctxLogger telemetry.Logger, phoneNumb number, err := phonenumbers.Parse(phoneNumber, phonenumbers.UNKNOWN_REGION) if err != nil { - ctxLogger.Error(stacktrace.Propagate(err, "cannot parse number [%s]", phoneNumber)) + ctxLogger.Error(stacktrace.Propagatef(err, "cannot parse number [%s]", phoneNumber)) return phoneNumber } diff --git a/api/pkg/services/user_service.go b/api/pkg/services/user_service.go index 265ad10fa..131b72d10 100644 --- a/api/pkg/services/user_service.go +++ b/api/pkg/services/user_service.go @@ -66,7 +66,7 @@ func (service *UserService) GetSubscriptionPayments(ctx context.Context, userID user, err := service.repository.Load(ctx, userID) if err != nil { - return invoices, service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "could not get [%T] with with ID [%s]", user, userID)) + return invoices, service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "could not get [%T] with with ID [%s]", user, userID)) } if user.SubscriptionID == nil { @@ -77,7 +77,7 @@ func (service *UserService) GetSubscriptionPayments(ctx context.Context, userID ctxLogger.Info(fmt.Sprintf("fetching subscription payments for [%T] with ID [%s] and subscription [%s]", user, user.ID, *user.SubscriptionID)) invoicesResponse, _, err := service.lemonsqueezyClient.SubscriptionInvoices.List(ctx, map[string]string{"filter[subscription_id]": *user.SubscriptionID}) if err != nil { - return invoices, service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "could not get invoices for subscription [%s] for [%T] with with ID [%s]", *user.SubscriptionID, user, user.ID)) + return invoices, service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "could not get invoices for subscription [%s] for [%T] with with ID [%s]", *user.SubscriptionID, user, user.ID)) } ctxLogger.Info(fmt.Sprintf("fetched [%d] payments for [%T] with ID [%s] and subscription ID [%s]", len(invoicesResponse.Data), user, user.ID, *user.SubscriptionID)) @@ -115,12 +115,12 @@ func (service *UserService) GenerateReceipt(ctx context.Context, params *UserInv invoice, _, err := service.lemonsqueezyClient.SubscriptionInvoices.Generate(ctx, params.SubscriptionInvoiceID, payload) if err != nil { - return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "could not generate subscription payment invoice user with ID [%s] and subscription invoice ID [%s]", params.UserID, params.SubscriptionInvoiceID)) + return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "could not generate subscription payment invoice user with ID [%s] and subscription invoice ID [%s]", params.UserID, params.SubscriptionInvoiceID)) } response, err := service.httpClient.Get(invoice.Meta.Urls.DownloadInvoice) if err != nil { - return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "could not download subscription payment invoice for user with ID [%s] and subscription invoice ID [%s]", params.UserID, params.SubscriptionInvoiceID)) + return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "could not download subscription payment invoice for user with ID [%s] and subscription invoice ID [%s]", params.UserID, params.SubscriptionInvoiceID)) } ctxLogger.Info(fmt.Sprintf("generated subscription payment invoice for user with ID [%s] and subscription invoice ID [%s]", params.UserID, params.SubscriptionInvoiceID)) @@ -134,7 +134,7 @@ func (service *UserService) Get(ctx context.Context, source string, authUser ent user, isNew, err := service.repository.LoadOrStore(ctx, authUser) if err != nil { - return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "could not get [%T] with from [%+#v]", user, authUser)) + return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "could not get [%T] with from [%+#v]", user, authUser)) } if isNew { @@ -153,12 +153,12 @@ func (service *UserService) dispatchUserCreatedEvent(ctx context.Context, source Timestamp: time.Now().UTC(), }) if err != nil { - ctxLogger.Error(stacktrace.Propagate(err, "cannot create event [%s] for user [%s]", events.UserAccountCreated, user.ID)) + ctxLogger.Error(stacktrace.Propagatef(err, "cannot create event [%s] for user [%s]", events.UserAccountCreated, user.ID)) return } if err = service.dispatcher.Dispatch(ctx, event); err != nil { - ctxLogger.Error(stacktrace.Propagate(err, "cannot dispatch [%s] event for user [%s]", event.Type(), user.ID)) + ctxLogger.Error(stacktrace.Propagatef(err, "cannot dispatch [%s] event for user [%s]", event.Type(), user.ID)) return } } @@ -170,7 +170,7 @@ func (service *UserService) GetByID(ctx context.Context, userID entities.UserID) user, err := service.repository.Load(ctx, userID) if err != nil { - return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "could not get [%T] with ID [%s]", user, userID)) + return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "could not get [%T] with ID [%s]", user, userID)) } return user, nil @@ -191,7 +191,7 @@ func (service *UserService) Update(ctx context.Context, source string, authUser user, isNew, err := service.repository.LoadOrStore(ctx, authUser) if err != nil { - return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "could not get [%T] with from [%+#v]", user, authUser)) + return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "could not get [%T] with from [%+#v]", user, authUser)) } if isNew { @@ -202,7 +202,7 @@ func (service *UserService) Update(ctx context.Context, source string, authUser user.ActivePhoneID = params.ActivePhoneID if err = service.repository.Update(ctx, user); err != nil { - return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot save user with id [%s]", user.ID)) + return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot save user with id [%s]", user.ID)) } ctxLogger.Info(fmt.Sprintf("user saved with id [%s] in the userRepository", user.ID)) @@ -224,7 +224,7 @@ func (service *UserService) UpdateNotificationSettings(ctx context.Context, user user, err := service.repository.Load(ctx, userID) if err != nil { - return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "could not load [%T] with ID [%s]", user, userID)) + return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "could not load [%T] with ID [%s]", user, userID)) } user.NotificationWebhookEnabled = params.WebhookEnabled @@ -233,7 +233,7 @@ func (service *UserService) UpdateNotificationSettings(ctx context.Context, user user.NotificationNewsletterEnabled = params.NewsletterEnabled if err = service.repository.Update(ctx, user); err != nil { - return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot save user with id [%s] in [%T]", user.ID, service.repository)) + return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot save user with id [%s] in [%T]", user.ID, service.repository)) } ctxLogger.Info(fmt.Sprintf("updated notification settings for [%T] with ID [%s] in the [%T]", user, user.ID, service.repository)) @@ -247,7 +247,7 @@ func (service *UserService) RotateAPIKey(ctx context.Context, source string, use user, err := service.repository.RotateAPIKey(ctx, userID) if err != nil { - return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "could not rotate API key for [%T] with ID [%s]", user, userID)) + return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "could not rotate API key for [%T] with ID [%s]", user, userID)) } ctxLogger.Info(fmt.Sprintf("rotated the api key for [%T] with ID [%s] in the [%T]", user, user.ID, service.repository)) @@ -259,12 +259,12 @@ func (service *UserService) RotateAPIKey(ctx context.Context, source string, use Timezone: user.Timezone, }) if err != nil { - ctxLogger.Error(stacktrace.Propagate(err, "cannot create event [%s] for user [%s]", events.UserAPIKeyRotated, user.ID)) + ctxLogger.Error(stacktrace.Propagatef(err, "cannot create event [%s] for user [%s]", events.UserAPIKeyRotated, user.ID)) return user, nil } if err = service.dispatcher.Dispatch(ctx, event); err != nil { - ctxLogger.Error(stacktrace.Propagate(err, "cannot dispatch [%s] event for user [%s]", event.Type(), user.ID)) + ctxLogger.Error(stacktrace.Propagatef(err, "cannot dispatch [%s] event for user [%s]", event.Type(), user.ID)) return user, nil } @@ -278,15 +278,15 @@ func (service *UserService) Delete(ctx context.Context, source string, userID en user, err := service.repository.Load(ctx, userID) if err != nil { - return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot load user with ID [%s] from the [%T]", userID, service.repository)) + return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot load user with ID [%s] from the [%T]", userID, service.repository)) } if !user.IsOnFreePlan() && user.SubscriptionRenewsAt != nil && user.SubscriptionRenewsAt.After(time.Now()) { - return service.tracer.WrapErrorSpan(span, stacktrace.NewError("cannot delete user with ID [%s] because they are have an active [%s] subscription which renews at [%s]", userID, user.SubscriptionName, user.SubscriptionRenewsAt)) + return service.tracer.WrapErrorSpan(span, stacktrace.NewErrorf("cannot delete user with ID [%s] because they are have an active [%s] subscription which renews at [%s]", userID, user.SubscriptionName, user.SubscriptionRenewsAt)) } if err = service.repository.Delete(ctx, user); err != nil { - return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "could not delete user with ID [%s] from the [%T]", userID, service.repository)) + return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "could not delete user with ID [%s] from the [%T]", userID, service.repository)) } ctxLogger.Info(fmt.Sprintf("sucessfully deleted user with ID [%s] in the [%T]", userID, service.repository)) @@ -297,11 +297,11 @@ func (service *UserService) Delete(ctx context.Context, source string, userID en Timestamp: time.Now().UTC(), }) if err != nil { - return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot create event [%s] for user [%s]", events.UserAccountDeleted, userID)) + return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot create event [%s] for user [%s]", events.UserAccountDeleted, userID)) } if err = service.dispatcher.Dispatch(ctx, event); err != nil { - return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot dispatch [%s] event for user [%s]", event.Type(), userID)) + return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot dispatch [%s] event for user [%s]", event.Type(), userID)) } return nil @@ -316,11 +316,11 @@ func (service *UserService) SendAPIKeyRotatedEmail(ctx context.Context, payload email, err := service.emailFactory.APIKeyRotated(payload.Email, payload.Timestamp, payload.Timezone) if err != nil { - return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot create api key rotated email for user [%s]", payload.UserID)) + return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot create api key rotated email for user [%s]", payload.UserID)) } if err = service.mailer.Send(ctx, email); err != nil { - return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "canot create api key rotated email to user [%s]", payload.UserID)) + return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "canot create api key rotated email to user [%s]", payload.UserID)) } ctxLogger.Info(fmt.Sprintf("api key rotated email sent successfully to [%s] with user ID [%s]", payload.Email, payload.UserID)) @@ -344,7 +344,7 @@ func (service *UserService) SendPhoneDeadEmail(ctx context.Context, params *User user, err := service.repository.Load(ctx, params.UserID) if err != nil { - return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "could not get [%T] with ID [%s]", user, params.UserID)) + return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "could not get [%T] with ID [%s]", user, params.UserID)) } if !user.NotificationHeartbeatEnabled { @@ -354,11 +354,11 @@ func (service *UserService) SendPhoneDeadEmail(ctx context.Context, params *User email, err := service.emailFactory.PhoneDead(user, params.LastHeartbeatTimestamp, params.Owner) if err != nil { - return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot create phone dead email for user [%s]", params.UserID)) + return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot create phone dead email for user [%s]", params.UserID)) } if err = service.mailer.Send(ctx, email); err != nil { - return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "canot send phone dead notification to user [%s]", params.UserID)) + return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "canot send phone dead notification to user [%s]", params.UserID)) } ctxLogger.Info(fmt.Sprintf("phone dead notification sent successfully to [%s] about [%s]", user.Email, params.Owner)) @@ -372,7 +372,7 @@ func (service *UserService) StartSubscription(ctx context.Context, params *event user, err := service.repository.Load(ctx, params.UserID) if err != nil { - return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "could not get [%T] with with ID [%s]", user, params.UserID)) + return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "could not get [%T] with with ID [%s]", user, params.UserID)) } user.SubscriptionID = ¶ms.SubscriptionID @@ -382,7 +382,7 @@ func (service *UserService) StartSubscription(ctx context.Context, params *event user.SubscriptionEndsAt = nil if err = service.repository.Update(ctx, user); err != nil { - return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "could not update [%T] with with ID [%s] after update", user, params.UserID)) + return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "could not update [%T] with with ID [%s] after update", user, params.UserID)) } return nil @@ -395,11 +395,11 @@ func (service *UserService) InitiateSubscriptionCancel(ctx context.Context, user user, err := service.repository.Load(ctx, userID) if err != nil { - return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "could not get [%T] with with ID [%s]", user, userID)) + return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "could not get [%T] with with ID [%s]", user, userID)) } if _, _, err = service.lemonsqueezyClient.Subscriptions.Cancel(ctx, *user.SubscriptionID); err != nil { - return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "could not cancel subscription [%s] for [%T] with with ID [%s]", *user.SubscriptionID, user, user.ID)) + return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "could not cancel subscription [%s] for [%T] with with ID [%s]", *user.SubscriptionID, user, user.ID)) } ctxLogger.Info(fmt.Sprintf("cancelled subscription [%s] for user [%s]", *user.SubscriptionID, user.ID)) @@ -413,12 +413,12 @@ func (service *UserService) GetSubscriptionUpdateURL(ctx context.Context, userID user, err := service.repository.Load(ctx, userID) if err != nil { - return "", service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "could not get [%T] with with ID [%s]", user, userID)) + return "", service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "could not get [%T] with with ID [%s]", user, userID)) } subscription, _, err := service.lemonsqueezyClient.Subscriptions.Get(ctx, *user.SubscriptionID) if err != nil { - return url, service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "could not get subscription [%s] for [%T] with with ID [%s]", *user.SubscriptionID, user, user.ID)) + return url, service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "could not get subscription [%s] for [%T] with with ID [%s]", *user.SubscriptionID, user, user.ID)) } return subscription.Data.Attributes.Urls.CustomerPortal, nil @@ -431,7 +431,7 @@ func (service *UserService) CancelSubscription(ctx context.Context, params *even user, err := service.repository.Load(ctx, params.UserID) if err != nil { - return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "could not get [%T] with with ID [%s]", user, params.UserID)) + return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "could not get [%T] with with ID [%s]", user, params.UserID)) } user.SubscriptionID = ¶ms.SubscriptionID @@ -441,7 +441,7 @@ func (service *UserService) CancelSubscription(ctx context.Context, params *even user.SubscriptionEndsAt = ¶ms.SubscriptionEndsAt if err = service.repository.Update(ctx, user); err != nil { - return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "could not update [%T] with with ID [%s] after update", user, params.UserID)) + return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "could not update [%T] with with ID [%s] after update", user, params.UserID)) } return nil @@ -454,7 +454,7 @@ func (service *UserService) ExpireSubscription(ctx context.Context, params *even user, err := service.repository.Load(ctx, params.UserID) if err != nil { - return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "could not get [%T] with with ID [%s]", user, params.UserID)) + return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "could not get [%T] with with ID [%s]", user, params.UserID)) } user.SubscriptionID = nil @@ -464,7 +464,7 @@ func (service *UserService) ExpireSubscription(ctx context.Context, params *even user.SubscriptionEndsAt = nil if err = service.repository.Update(ctx, user); err != nil { - return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "could not update [%T] with with ID [%s] after expired subscription update", user, params.UserID)) + return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "could not update [%T] with with ID [%s] after expired subscription update", user, params.UserID)) } return nil @@ -477,7 +477,7 @@ func (service *UserService) UpdateSubscription(ctx context.Context, params *even user, err := service.repository.Load(ctx, params.UserID) if err != nil { - return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "could not get [%T] with with ID [%s]", user, params.UserID)) + return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "could not get [%T] with with ID [%s]", user, params.UserID)) } if params.SubscriptionStatus != "active" { @@ -493,7 +493,7 @@ func (service *UserService) UpdateSubscription(ctx context.Context, params *even user.SubscriptionStatus = ¶ms.SubscriptionStatus if err = service.repository.Update(ctx, user); err != nil { - return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "could not update [%T] with with ID [%s] after subscription update", user, params.UserID)) + return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "could not update [%T] with with ID [%s] after subscription update", user, params.UserID)) } return nil @@ -505,7 +505,7 @@ func (service *UserService) DeleteAuthUser(ctx context.Context, userID entities. defer span.End() if err := service.authClient.DeleteUser(ctx, userID.String()); err != nil { - return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "could not delete [entities.AuthContext] from firebase with ID [%s]", userID)) + return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "could not delete [entities.AuthContext] from firebase with ID [%s]", userID)) } ctxLogger.Info(fmt.Sprintf("deleted [entities.AuthContext] from firebase for user with ID [%s]", userID)) diff --git a/api/pkg/services/webhook_service.go b/api/pkg/services/webhook_service.go index 581fa202d..ee5918d37 100644 --- a/api/pkg/services/webhook_service.go +++ b/api/pkg/services/webhook_service.go @@ -61,7 +61,7 @@ func (service *WebhookService) DeleteAllForUser(ctx context.Context, userID enti defer span.End() if err := service.repository.DeleteAllForUser(ctx, userID); err != nil { - return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "could not delete all [entities.Webhook] for user with ID [%s]", userID)) + return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "could not delete all [entities.Webhook] for user with ID [%s]", userID)) } ctxLogger.Info(fmt.Sprintf("deleted all [entities.Webhook] for user with ID [%s]", userID)) @@ -77,7 +77,7 @@ func (service *WebhookService) Index(ctx context.Context, userID entities.UserID webhooks, err := service.repository.Index(ctx, userID, params) if err != nil { - return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "could not fetch webhooks with params [%+#v]", params)) + return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "could not fetch webhooks with params [%+#v]", params)) } ctxLogger.Info(fmt.Sprintf("fetched [%d] webhooks with prams [%+#v]", len(webhooks), params)) @@ -92,11 +92,11 @@ func (service *WebhookService) Delete(ctx context.Context, userID entities.UserI ctxLogger := service.tracer.CtxLogger(service.logger, span) if _, err := service.repository.Load(ctx, userID, webhookID); err != nil { - return service.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCode(err, stacktrace.GetCode(err), "cannot load webhook with userID [%s] and phoneID [%s]", userID, webhookID)) + return service.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCodef(err, stacktrace.GetCode(err), "cannot load webhook with userID [%s] and phoneID [%s]", userID, webhookID)) } if err := service.repository.Delete(ctx, userID, webhookID); err != nil { - return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot delete webhook with id [%s] and user id [%s]", webhookID, userID)) + return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot delete webhook with id [%s] and user id [%s]", webhookID, userID)) } ctxLogger.Info(fmt.Sprintf("deleted webhook with id [%s] and user id [%s]", webhookID, userID)) @@ -131,7 +131,7 @@ func (service *WebhookService) Store(ctx context.Context, params *WebhookStorePa } if err := service.repository.Save(ctx, webhook); err != nil { - return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot save webhook with id [%s]", webhook.ID)) + return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot save webhook with id [%s]", webhook.ID)) } ctxLogger.Info(fmt.Sprintf("webhook saved with id [%s] for user [%s] in the [%T]", webhook.ID, webhook.UserID, service.repository)) @@ -155,7 +155,7 @@ func (service *WebhookService) Update(ctx context.Context, params *WebhookUpdate webhook, err := service.repository.Load(ctx, params.UserID, params.WebhookID) if err != nil { - return nil, service.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCode(err, stacktrace.GetCode(err), "cannot load webhook with userID [%s] and phoneID [%s]", params.UserID, params.WebhookID)) + return nil, service.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCodef(err, stacktrace.GetCode(err), "cannot load webhook with userID [%s] and phoneID [%s]", params.UserID, params.WebhookID)) } webhook.URL = params.URL @@ -164,7 +164,7 @@ func (service *WebhookService) Update(ctx context.Context, params *WebhookUpdate webhook.PhoneNumbers = params.PhoneNumbers if err = service.repository.Save(ctx, webhook); err != nil { - return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot save webhook with id [%s] after update", webhook.ID)) + return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot save webhook with id [%s] after update", webhook.ID)) } ctxLogger.Info(fmt.Sprintf("webhook updated with id [%s] in the [%T]", webhook.ID, service.repository)) @@ -178,7 +178,7 @@ func (service *WebhookService) Send(ctx context.Context, userID entities.UserID, webhooks, err := service.repository.LoadByEvent(ctx, userID, event.Type(), phoneNumber) if err != nil { - return service.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCode(err, stacktrace.GetCode(err), "cannot load webhooks for userID [%s] and event [%s]", userID, event.Type())) + return service.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCodef(err, stacktrace.GetCode(err), "cannot load webhooks for userID [%s] and event [%s]", userID, event.Type())) } if len(webhooks) == 0 { @@ -212,12 +212,12 @@ func (service *WebhookService) sendNotification(ctx context.Context, event cloud request, err := service.createRequest(requestCtx, event, webhook) if err != nil { - return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot create [%s] event to webhook [%s] for user [%s] after [%d] attempts", event.Type(), webhook.URL, webhook.UserID, attempts)) + return service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot create [%s] event to webhook [%s] for user [%s] after [%d] attempts", event.Type(), webhook.URL, webhook.UserID, attempts)) } response, err := service.client.Do(request) if err != nil { - ctxLogger.Warn(stacktrace.Propagate(err, "cannot send [%s] event to webhook [%s] for user [%s] after [%d] attempts", event.Type(), webhook.URL, webhook.UserID, attempts)) + ctxLogger.Warn(stacktrace.Propagatef(err, "cannot send [%s] event to webhook [%s] for user [%s] after [%d] attempts", event.Type(), webhook.URL, webhook.UserID, attempts)) if attempts == 1 { return err } @@ -228,16 +228,16 @@ func (service *WebhookService) sendNotification(ctx context.Context, event cloud defer func() { err = response.Body.Close() if err != nil { - ctxLogger.Error(stacktrace.Propagate(err, "cannot close response body for [%s] event with ID [%s] after [%d] attempts", event.Type(), event.ID(), attempts)) + ctxLogger.Error(stacktrace.Propagatef(err, "cannot close response body for [%s] event with ID [%s] after [%d] attempts", event.Type(), event.ID(), attempts)) } }() if response.StatusCode >= 400 { ctxLogger.Info(fmt.Sprintf("cannot send [%s] event to webhook [%s] for user [%s] with response code [%d] after [%d] attempts", event.Type(), webhook.URL, webhook.UserID, response.StatusCode, attempts)) if attempts == 1 { - return stacktrace.NewError("%s", http.StatusText(response.StatusCode)) + return stacktrace.NewErrorf("%s", http.StatusText(response.StatusCode)) } - service.handleWebhookSendFailed(ctx, event, webhook, owner, stacktrace.NewError("%s", http.StatusText(response.StatusCode)), response) + service.handleWebhookSendFailed(ctx, event, webhook, owner, stacktrace.NewErrorf("%s", http.StatusText(response.StatusCode)), response) return nil } @@ -245,7 +245,7 @@ func (service *WebhookService) sendNotification(ctx context.Context, event cloud return nil }) if err != nil { - ctxLogger.Error(service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot handle [%s] event to webhook [%s] for user [%s] after [%d] attempts", event.Type(), webhook.URL, webhook.UserID, attempts))) + ctxLogger.Error(service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot handle [%s] event to webhook [%s] for user [%s] after [%d] attempts", event.Type(), webhook.URL, webhook.UserID, attempts))) } } @@ -255,12 +255,12 @@ func (service *WebhookService) createRequest(ctx context.Context, event cloudeve payload, err := json.Marshal(service.getPayload(ctxLogger, event, webhook)) if err != nil { - return nil, stacktrace.Propagate(err, "cannot marshal payload for user [%s] and webhook [%s] for event [%s]", webhook.UserID, webhook.ID, event.ID()) + return nil, stacktrace.Propagatef(err, "cannot marshal payload for user [%s] and webhook [%s] for event [%s]", webhook.UserID, webhook.ID, event.ID()) } request, err := http.NewRequestWithContext(ctx, http.MethodPost, webhook.URL, bytes.NewReader(payload)) if err != nil { - return nil, stacktrace.Propagate(err, "cannot create request for user [%s] and webhook [%s] for event [%s]", webhook.UserID, webhook.ID, event.ID()) + return nil, stacktrace.Propagatef(err, "cannot create request for user [%s] and webhook [%s] for event [%s]", webhook.UserID, webhook.ID, event.ID()) } request.Header.Add("X-Event-Type", event.Type()) @@ -269,7 +269,7 @@ func (service *WebhookService) createRequest(ctx context.Context, event cloudeve if strings.TrimSpace(webhook.SigningKey) != "" { token, err := service.getAuthToken(webhook) if err != nil { - return nil, stacktrace.Propagate(err, "cannot generate auth token for user [%s] and webhook [%s]", webhook.UserID, webhook.ID) + return nil, stacktrace.Propagatef(err, "cannot generate auth token for user [%s] and webhook [%s]", webhook.UserID, webhook.ID) } request.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token)) } @@ -290,7 +290,7 @@ func (service *WebhookService) getPayload(ctxLogger telemetry.Logger, event clou err := event.DataAs(payload) if err != nil { - ctxLogger.Error(stacktrace.Propagate(err, "cannot unmarshal event [%s] with ID [%s] into [%T]", event.Type(), event.ID(), payload)) + ctxLogger.Error(stacktrace.Propagatef(err, "cannot unmarshal event [%s] with ID [%s] into [%T]", event.Type(), event.ID(), payload)) return event } @@ -369,12 +369,12 @@ func (service *WebhookService) handleWebhookSendFailed(ctx context.Context, even event, err = service.createEvent(events.EventTypeWebhookSendFailed, event.Source(), payload) if err != nil { - ctxLogger.Error(service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot create event [%s] for user with id [%s]", events.EventTypeWebhookSendFailed, payload.UserID))) + ctxLogger.Error(service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot create event [%s] for user with id [%s]", events.EventTypeWebhookSendFailed, payload.UserID))) return } if err = service.dispatcher.Dispatch(ctx, event); err != nil { - ctxLogger.Error(service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot dispatch event [%s] for user with id [%s]", event.Type(), payload.UserID))) + ctxLogger.Error(service.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot dispatch event [%s] for user with id [%s]", event.Type(), payload.UserID))) return } diff --git a/api/pkg/telemetry/gorm_logger.go b/api/pkg/telemetry/gorm_logger.go index 9107afcce..7f9d9517c 100644 --- a/api/pkg/telemetry/gorm_logger.go +++ b/api/pkg/telemetry/gorm_logger.go @@ -46,7 +46,7 @@ func (gorm *gormLogger) Trace(ctx context.Context, begin time.Time, fc func() (s msg := fmt.Sprintf("[ROWS:%d][%s]", rows, sql) if err != nil { - l.Error(stacktrace.Propagate(err, "[ROWS:%d][%s]", rows, sql)) + l.Error(stacktrace.Propagatef(err, "[ROWS:%d][%s]", rows, sql)) return } diff --git a/api/pkg/validators/bulk_message_handler_validator.go b/api/pkg/validators/bulk_message_handler_validator.go index 8b9b926d2..588407ac8 100644 --- a/api/pkg/validators/bulk_message_handler_validator.go +++ b/api/pkg/validators/bulk_message_handler_validator.go @@ -60,7 +60,7 @@ func (v *BulkMessageHandlerValidator) ValidateStore(ctx context.Context, userID if err != nil { result := url.Values{} result.Add("document", "Cannot load your account. Please try again later or contact support.") - ctxLogger.Error(v.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot load user [%s]", userID))) + ctxLogger.Error(v.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot load user [%s]", userID))) return nil, nil, result } @@ -104,7 +104,7 @@ func (v *BulkMessageHandlerValidator) parseFile(ctxLogger telemetry.Logger, user return v.parseXlsx(ctxLogger, user, header) } - ctxLogger.Error(stacktrace.NewError("cannot parse file [%s] for user [%s] with content type [%s]", header.Filename, user.ID, header.Header.Get("Content-Type"))) + ctxLogger.Error(stacktrace.NewErrorf("cannot parse file [%s] for user [%s] with content type [%s]", header.Filename, user.ID, header.Header.Get("Content-Type"))) result := url.Values{} result.Add("document", fmt.Sprintf("The file [%s] is not a valid CSV or Excel file.", header.Filename)) @@ -119,7 +119,7 @@ func (v *BulkMessageHandlerValidator) parseXlsx(ctxLogger telemetry.Logger, user excel, err := excelize.OpenReader(bytes.NewReader(content)) if err != nil { - ctxLogger.Error(stacktrace.Propagate(err, "cannot generate excel file from [%s] for user [%s]", header.Filename, user.ID)) + ctxLogger.Error(stacktrace.Propagatef(err, "cannot generate excel file from [%s] for user [%s]", header.Filename, user.ID)) result.Add("document", fmt.Sprintf("Cannot parse the uploaded excel file with name [%s].", header.Filename)) return nil, result } @@ -127,7 +127,7 @@ func (v *BulkMessageHandlerValidator) parseXlsx(ctxLogger telemetry.Logger, user rows, err := excel.GetRows(excel.GetSheetName(0)) if err != nil { - ctxLogger.Error(stacktrace.Propagate(err, "cannot get rows from excel file [%s] for user [%s]", header.Filename, user.ID)) + ctxLogger.Error(stacktrace.Propagatef(err, "cannot get rows from excel file [%s] for user [%s]", header.Filename, user.ID)) result.Add("document", fmt.Sprintf("Cannot parse the uploaded excel file with name [%s].", header.Filename)) return nil, result } @@ -177,19 +177,19 @@ func (v *BulkMessageHandlerValidator) parseBytes(ctxLogger telemetry.Logger, use file, err := header.Open() if err != nil { - ctxLogger.Error(stacktrace.Propagate(err, "cannot open file [%s] for reading for user [%s]", header.Filename, userID)) + ctxLogger.Error(stacktrace.Propagatef(err, "cannot open file [%s] for reading for user [%s]", header.Filename, userID)) result.Add("document", fmt.Sprintf("Cannot open the uploaded file with name [%s].", header.Filename)) return nil, result } defer func() { if e := file.Close(); e != nil { - ctxLogger.Error(stacktrace.Propagate(e, "cannot close file [%s] for user [%s]", header.Filename, userID)) + ctxLogger.Error(stacktrace.Propagatef(e, "cannot close file [%s] for user [%s]", header.Filename, userID)) } }() b := new(bytes.Buffer) if _, err = io.Copy(b, file); err != nil { - ctxLogger.Error(stacktrace.Propagate(err, "cannot copy file [%s] to buffer for user [%s]", header.Filename, userID)) + ctxLogger.Error(stacktrace.Propagatef(err, "cannot copy file [%s] to buffer for user [%s]", header.Filename, userID)) result.Add("document", fmt.Sprintf("Cannot read the conents of the uploaded file [%s].", header.Filename)) return nil, result } @@ -205,7 +205,7 @@ func (v *BulkMessageHandlerValidator) parseCSV(ctxLogger telemetry.Logger, user var messages []*requests.BulkMessage if err := csvutil.Unmarshal(content, &messages); err != nil { - ctxLogger.Error(stacktrace.Propagate(err, "cannot unmarshall contents [%s] into type [%T] for file [%s] and user [%s]", content, messages, header.Filename, user.ID)) + ctxLogger.Error(stacktrace.Propagatef(err, "cannot unmarshall contents [%s] into type [%T] for file [%s] and user [%s]", content, messages, header.Filename, user.ID)) result.Add("document", fmt.Sprintf("Cannot read the contents of the uploaded file [%s].", header.Filename)) return nil, result } diff --git a/api/pkg/validators/discord_handler_validator.go b/api/pkg/validators/discord_handler_validator.go index 035777ceb..69f4284ef 100644 --- a/api/pkg/validators/discord_handler_validator.go +++ b/api/pkg/validators/discord_handler_validator.go @@ -91,12 +91,12 @@ func (validator *DiscordHandlerValidator) ValidateStore(ctx context.Context, req } if _, _, err := validator.client.Channel.Get(ctx, request.IncomingChannelID); err != nil { - ctxLogger.Error(validator.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot fetch discord channel with ID [%s]", request.IncomingChannelID))) + ctxLogger.Error(validator.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot fetch discord channel with ID [%s]", request.IncomingChannelID))) result.Add("incoming_channel_id", fmt.Sprintf("cannot fetch discord channel with ID [%s] make sure the bot has access to the channel", request.IncomingChannelID)) } if _, _, err := validator.client.Guild.Get(ctx, request.ServerID); err != nil { - ctxLogger.Error(validator.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot fetch discord server with ID [%s]", request.ServerID))) + ctxLogger.Error(validator.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot fetch discord server with ID [%s]", request.ServerID))) result.Add("server_id", fmt.Sprintf("cannot fetch discord server with ID [%s] make sure the bot has access to the server", request.ServerID)) } @@ -139,12 +139,12 @@ func (validator *DiscordHandlerValidator) ValidateUpdate(ctx context.Context, re } if _, _, err := validator.client.Channel.Get(ctx, request.IncomingChannelID); err != nil { - ctxLogger.Error(validator.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot fetch discord channel with ID [%s]", request.IncomingChannelID))) + ctxLogger.Error(validator.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot fetch discord channel with ID [%s]", request.IncomingChannelID))) result.Add("incoming_channel_id", fmt.Sprintf("cannot fetch discord channel with ID [%s] make sure the bot has access to the channel", request.IncomingChannelID)) } if _, _, err := validator.client.Guild.Get(ctx, request.ServerID); err != nil { - ctxLogger.Error(validator.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot fetch discord server with ID [%s]", request.ServerID))) + ctxLogger.Error(validator.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot fetch discord server with ID [%s]", request.ServerID))) result.Add("server_id", fmt.Sprintf("cannot fetch discord server with ID [%s] make sure the bot has access to the server", request.ServerID)) } diff --git a/api/pkg/validators/message_handler_validator.go b/api/pkg/validators/message_handler_validator.go index 998b4d804..d4da9e109 100644 --- a/api/pkg/validators/message_handler_validator.go +++ b/api/pkg/validators/message_handler_validator.go @@ -179,7 +179,7 @@ func (validator MessageHandlerValidator) ValidateMessageSend(ctx context.Context } if err != nil { - ctxLogger.Error(validator.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "could not load phone for user [%s] and phone [%s]", userID, request.From))) + ctxLogger.Error(validator.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "could not load phone for user [%s] and phone [%s]", userID, request.From))) result.Add("from", fmt.Sprintf("could not validate 'from' number [%s], please try again later", request.From)) } @@ -229,7 +229,7 @@ func (validator MessageHandlerValidator) ValidateMessageBulkSend(ctx context.Con } if err != nil { - ctxLogger.Error(validator.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "could not load phone for user [%s] and phone [%s]", userID, request.From))) + ctxLogger.Error(validator.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "could not load phone for user [%s] and phone [%s]", userID, request.From))) result.Add("from", fmt.Sprintf("could not validate 'from' number [%s], please try again later", request.From)) } diff --git a/api/pkg/validators/turnstile_token_validator.go b/api/pkg/validators/turnstile_token_validator.go index 5d30b4601..ac1a3d3e5 100644 --- a/api/pkg/validators/turnstile_token_validator.go +++ b/api/pkg/validators/turnstile_token_validator.go @@ -55,27 +55,27 @@ func (v *TurnstileTokenValidator) ValidateToken(ctx context.Context, ipAddress, "remoteip": ipAddress, }) if err != nil { - ctxLogger.Error(stacktrace.Propagate(err, "failed to marshal payload")) + ctxLogger.Error(stacktrace.Propagatef(err, "failed to marshal payload")) return false } request, err := http.NewRequestWithContext(ctx, http.MethodPost, "https://challenges.cloudflare.com/turnstile/v0/siteverify", bytes.NewBuffer(payload)) if err != nil { - ctxLogger.Error(stacktrace.Propagate(err, "failed to create http request request")) + ctxLogger.Error(stacktrace.Propagatef(err, "failed to create http request request")) return false } request.Header.Set("Content-Type", "application/json") response, err := v.httpClient.Do(request) if err != nil { - ctxLogger.Error(stacktrace.Propagate(err, "failed to send http request to [%s]", request.URL.String())) + ctxLogger.Error(stacktrace.Propagatef(err, "failed to send http request to [%s]", request.URL.String())) return false } defer response.Body.Close() body, err := io.ReadAll(response.Body) if err != nil { - ctxLogger.Error(stacktrace.Propagate(err, "failed to read response body from cloudflare turnstile")) + ctxLogger.Error(stacktrace.Propagatef(err, "failed to read response body from cloudflare turnstile")) return false } @@ -83,7 +83,7 @@ func (v *TurnstileTokenValidator) ValidateToken(ctx context.Context, ipAddress, data := new(turnstileVerifyResponse) if err = json.Unmarshal(body, data); err != nil { - ctxLogger.Error(stacktrace.Propagate(err, "failed to unmarshal response from cloudflare turnstile")) + ctxLogger.Error(stacktrace.Propagatef(err, "failed to unmarshal response from cloudflare turnstile")) return false } diff --git a/api/pkg/validators/user_handler_validator.go b/api/pkg/validators/user_handler_validator.go index ee163d821..42a6b91f9 100644 --- a/api/pkg/validators/user_handler_validator.go +++ b/api/pkg/validators/user_handler_validator.go @@ -112,7 +112,7 @@ func (validator *UserHandlerValidator) ValidatePaymentInvoice(ctx context.Contex payments, err := validator.service.GetSubscriptionPayments(ctx, userID) if err != nil { - ctxLogger.Error(validator.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, "cannot get subscription payments for user with ID [%s]", userID))) + ctxLogger.Error(validator.tracer.WrapErrorSpan(span, stacktrace.Propagatef(err, "cannot get subscription payments for user with ID [%s]", userID))) validationErrors.Add("subscriptionInvoiceID", "failed to validate subscription payment invoice ID") return validationErrors } From 96ffb5c7788602e2f743a0d876de58967bca666f Mon Sep 17 00:00:00 2001 From: Acho Arnold Date: Sun, 19 Jul 2026 19:50:37 +0300 Subject: [PATCH 07/88] feat(web): redirect logged-in users from root to /threads (opt-in) (#950) * docs: add design spec for root->threads redirect Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 6e6a8ddc-a5d6-49dc-99c9-f9b7fcbc00ce * docs: add implementation plan for root->threads redirect Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 6e6a8ddc-a5d6-49dc-99c9-f9b7fcbc00ce * feat(web): add redirectPreference store for per-browser threads redirect * feat(web): optimistically redirect root to /threads when opted in Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat(web): add redirect opt-in popover component * feat(web): mount redirect opt-in popover under Dashboard button * feat(web): clear threads-redirect preference on logout Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(web): use replace navigation in redirect enable() to avoid stuck Back button Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 6e6a8ddc-a5d6-49dc-99c9-f9b7fcbc00ce * ci(web): pin pnpm to 11.11.0 to avoid installer bug pnpm 11.12.0 crashes pnpm/action-setup (pnpm/action-setup#276). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 6e6a8ddc-a5d6-49dc-99c9-f9b7fcbc00ce * fix(web): import ref from vue, drop gitignored docs Resolves Codacy type findings; removes gitignored docs/ files. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 6e6a8ddc-a5d6-49dc-99c9-f9b7fcbc00ce * fix(web): persist redirect before navigation Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 878f9e23-78ea-4f5d-aed2-c1381514107f * fix(web): gate landing nav auth buttons on authStateChanged On CTRL+R reload of /, Firebase restores the session asynchronously. The website layout rendered Login/Get Started while authUser was still null, hiding the Dashboard button until the state resolved. Wait for authStateChanged before showing any auth-dependent button, matching the default (dashboard) layout. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 878f9e23-78ea-4f5d-aed2-c1381514107f * refactor(web): render redirect prompt as v-menu not v-card Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 878f9e23-78ea-4f5d-aed2-c1381514107f --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/web.yml | 4 +- web/app/components/MessageThreadHeader.vue | 2 + web/app/components/RedirectPromptPopover.vue | 59 ++++++++++++++++++++ web/app/layouts/website.vue | 31 ++++++---- web/app/middleware/redirectToThreads.ts | 11 ++++ web/app/pages/index.vue | 1 + web/app/pages/settings/index.vue | 2 + web/app/stores/redirectPreference.ts | 47 ++++++++++++++++ 8 files changed, 144 insertions(+), 13 deletions(-) create mode 100644 web/app/components/RedirectPromptPopover.vue create mode 100644 web/app/middleware/redirectToThreads.ts create mode 100644 web/app/stores/redirectPreference.ts diff --git a/.github/workflows/web.yml b/.github/workflows/web.yml index 350bf3256..fd5ea84e6 100644 --- a/.github/workflows/web.yml +++ b/.github/workflows/web.yml @@ -34,7 +34,7 @@ jobs: - uses: pnpm/action-setup@v6 name: Install pnpm with: - version: 11 + version: 11.11.0 - name: Install dependencies 📦 run: pnpm install @@ -66,7 +66,7 @@ jobs: - uses: pnpm/action-setup@v6 name: Install pnpm with: - version: 11 + version: 11.11.0 - name: Install dependencies 📦 run: pnpm install diff --git a/web/app/components/MessageThreadHeader.vue b/web/app/components/MessageThreadHeader.vue index 6d13a7e20..1e1a81eb2 100644 --- a/web/app/components/MessageThreadHeader.vue +++ b/web/app/components/MessageThreadHeader.vue @@ -27,6 +27,7 @@ const phonesStore = usePhonesStore() const threadsStore = useThreadsStore() const appStore = useAppStore() const notificationsStore = useNotificationsStore() +const redirectPreferenceStore = useRedirectPreferenceStore() const selectedMenuItem = ref(-1) @@ -71,6 +72,7 @@ async function logout() { authStore.resetState() phonesStore.resetState() threadsStore.resetState() + redirectPreferenceStore.resetState() notificationsStore.addNotification({ type: 'info', message: 'You have successfully logged out', diff --git a/web/app/components/RedirectPromptPopover.vue b/web/app/components/RedirectPromptPopover.vue new file mode 100644 index 000000000..859a71dac --- /dev/null +++ b/web/app/components/RedirectPromptPopover.vue @@ -0,0 +1,59 @@ + + + diff --git a/web/app/layouts/website.vue b/web/app/layouts/website.vue index 1aadb1b64..3b6565a5a 100644 --- a/web/app/layouts/website.vue +++ b/web/app/layouts/website.vue @@ -80,7 +80,11 @@ function goToPricing() { Blog  For Free - - Dashboard - + + Dashboard + + +
diff --git a/web/app/middleware/redirectToThreads.ts b/web/app/middleware/redirectToThreads.ts new file mode 100644 index 000000000..a605286fb --- /dev/null +++ b/web/app/middleware/redirectToThreads.ts @@ -0,0 +1,11 @@ +import { STORAGE_KEY } from '~/stores/redirectPreference' + +export default defineNuxtRouteMiddleware(() => { + try { + if (localStorage.getItem(STORAGE_KEY) === 'true') { + return navigateTo('/threads', { replace: true }) + } + } catch (error) { + console.error(error) + } +}) diff --git a/web/app/pages/index.vue b/web/app/pages/index.vue index 524c6637b..9790b5e7f 100644 --- a/web/app/pages/index.vue +++ b/web/app/pages/index.vue @@ -28,6 +28,7 @@ import { definePageMeta({ layout: 'website', + middleware: ['redirect-to-threads'], }) useSeoMeta({ diff --git a/web/app/pages/settings/index.vue b/web/app/pages/settings/index.vue index ea1752533..1f381416b 100644 --- a/web/app/pages/settings/index.vue +++ b/web/app/pages/settings/index.vue @@ -47,6 +47,7 @@ const authStore = useAuthStore() const phonesStore = usePhonesStore() const billingStore = useBillingStore() const notificationsStore = useNotificationsStore() +const redirectPreferenceStore = useRedirectPreferenceStore() const firebaseUser = ref(null) const gravatarUrl = ref(null) @@ -773,6 +774,7 @@ async function deleteUserAccount() { await signOut(auth) authStore.resetState() phonesStore.resetState() + redirectPreferenceStore.resetState() notificationsStore.addNotification({ type: 'info', message: 'You have successfully logged out', diff --git a/web/app/stores/redirectPreference.ts b/web/app/stores/redirectPreference.ts new file mode 100644 index 000000000..030def894 --- /dev/null +++ b/web/app/stores/redirectPreference.ts @@ -0,0 +1,47 @@ +import { defineStore } from 'pinia' +import { ref } from 'vue' + +export const STORAGE_KEY = 'httpsms_redirect_to_threads' + +function readFlag(): boolean { + try { + return localStorage.getItem(STORAGE_KEY) === 'true' + } catch (error) { + console.error(error) + return false + } +} + +export const useRedirectPreferenceStore = defineStore( + 'redirectPreference', + () => { + const enabled = ref(readFlag()) + const dismissedThisSession = ref(false) + + function enable() { + try { + localStorage.setItem(STORAGE_KEY, 'true') + enabled.value = true + navigateTo('/threads', { replace: true }) + } catch (error) { + console.error(error) + } + } + + function dismiss() { + dismissedThisSession.value = true + } + + function resetState() { + enabled.value = false + dismissedThisSession.value = false + try { + localStorage.removeItem(STORAGE_KEY) + } catch (error) { + console.error(error) + } + } + + return { enabled, dismissedThisSession, enable, dismiss, resetState } + }, +) From 9444545e67e9cd904095fc1acc81c15d315f5d0b Mon Sep 17 00:00:00 2001 From: Acho Arnold Ewin Date: Sun, 19 Jul 2026 19:51:31 +0300 Subject: [PATCH 08/88] style: add arrow position --- web/app/components/RedirectPromptPopover.vue | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/web/app/components/RedirectPromptPopover.vue b/web/app/components/RedirectPromptPopover.vue index 859a71dac..c2de18200 100644 --- a/web/app/components/RedirectPromptPopover.vue +++ b/web/app/components/RedirectPromptPopover.vue @@ -31,7 +31,7 @@ const menuOpen = computed({ :open-on-click="false" :close-on-content-click="false" > - + Skip this page next time? @@ -49,10 +49,9 @@ const menuOpen = computed({ - Always open dashboard - + Always open dashboard From 78e1d183a13f5e43143fba2c6dae1d883c05dcc5 Mon Sep 17 00:00:00 2001 From: Acho Arnold Ewin Date: Sun, 19 Jul 2026 20:13:15 +0300 Subject: [PATCH 09/88] docs: add contacts feature design spec Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 8dd82cae-8bc6-4eaa-9b90-95073720c577 --- .../2026-07-19-contacts-feature-design.md | 252 ++++++++++++++++++ 1 file changed, 252 insertions(+) create mode 100644 docs/superpowers/specs/2026-07-19-contacts-feature-design.md diff --git a/docs/superpowers/specs/2026-07-19-contacts-feature-design.md b/docs/superpowers/specs/2026-07-19-contacts-feature-design.md new file mode 100644 index 000000000..58f9d9e40 --- /dev/null +++ b/docs/superpowers/specs/2026-07-19-contacts-feature-design.md @@ -0,0 +1,252 @@ +# Contacts Feature — Design + +Date: 2026-07-19 +Status: Approved (pending implementation plan) + +## 1. Summary + +Add a **Contacts** feature to httpSMS. Users can store contacts (name, emails, +phone numbers, and free-form properties) and manage them through a dedicated web +page and a REST API. Once contacts exist, the threads page displays the +contact's **name** instead of the raw phone number. + +Design priority (explicit user goal): resolve contact names into the threads +page with **as few database queries as possible**, and make **changing a +contact's name cheap** (no write amplification across threads). + +## 2. Goals + +- CRUD for contacts via a REST API under `/v1/contacts`. +- A single create endpoint that accepts **one or many** contacts in one request. +- Import contacts from **CSV** (Excel/XLSX is **not** supported for contacts). +- A contact has: `Name` (required), `Emails` (optional array), `PhoneNumbers` + (array, >= 1), and free-form `Properties` (`map[string]string`). +- Contacts are **global to the user account** (name shows across all the user's + owner phones). +- Threads display the resolved contact name (and expose full contact details) + when requested, everywhere the peer number is shown (threads list + thread + header/title). +- Dedicated Contacts web page with full CRUD + import. + +## 3. Non-goals / Explicit decisions + +- **No uniqueness constraint** on phone numbers. Multiple contacts may share a + phone number. There is **no** normalized `contact_phone_numbers` lookup table + and **no** DB-level unique index on numbers. +- When a thread's number matches **multiple** contacts, the **most recently + updated** contact wins for display. +- Thread-name resolution is **opt-in** per request (default off). +- `Properties` are API/JSON only; not part of the flat CSV template. + +## 4. Data model + +### 4.1 `Contact` entity (`api/pkg/entities/contact.go`) + +| Field | Type | GORM / notes | +| -------------- | ------------------- | --------------------------------------------- | +| `ID` | `uuid.UUID` | `primaryKey;type:uuid` | +| `UserID` | `entities.UserID` | indexed | +| `Name` | `string` | required | +| `Emails` | `pq.StringArray` | `gorm:"type:text[]"` `swaggertype:"array,string"`; optional, each validated | +| `PhoneNumbers` | `pq.StringArray` | `gorm:"type:text[]"` `swaggertype:"array,string"`; >= 1, each valid E.164 | +| `Properties` | `ContactProperties` | `gorm:"type:jsonb"`; free-form key/value map | +| `CreatedAt` | `time.Time` | | +| `UpdatedAt` | `time.Time` | | + +Follows the existing `pq.StringArray` convention used in `webhook.go` and +`phone_api_key.go`. Auto-migrated in `pkg/di/container.go` alongside the other +entities. + +### 4.2 `ContactProperties` custom type + +A small custom type to avoid adding a new dependency (`gorm.io/datatypes` is not +currently used): + +```go +// ContactProperties is a free-form key/value map persisted as a jsonb column. +type ContactProperties map[string]string + +func (p ContactProperties) Value() (driver.Value, error) // json.Marshal -> []byte +func (p *ContactProperties) Scan(src any) error // json.Unmarshal from []byte/string +``` + +Includes unit tests for `Value`/`Scan` round-trips (nil, empty, populated). + +### 4.3 `MessageThread` additions (`api/pkg/entities/message_thread.go`) + +Add a **non-persisted** field carrying the resolved contact: + +```go +ContactDetails *Contact `json:"contact_details,omitempty" gorm:"-"` +``` + +- `gorm:"-"` — never read/written to the DB. +- `omitempty` — absent from responses unless resolution attached it. +- Named `ContactDetails` (JSON `contact_details`) to avoid colliding with the + existing `Contact string json:"contact"` field, which holds the peer phone + number. + +## 5. API + +New `ContactHandler` mirroring existing handler/service/repository/validator +layering. Routes registered via the `handler.register(...)` helper (Fiber v3), +wired in the DI container. All routes are under `/v1/` and behind the standard +auth middleware chain. + +| Method | Route | Purpose | +| -------- | ------------------------ | ------------------------------------------------------------- | +| `GET` | `/v1/contacts` | List (paginated; `?query=` searches name / email / number) | +| `POST` | `/v1/contacts` | Create **one or many** — body is a JSON array of contacts | +| `POST` | `/v1/contacts/upload` | Import from CSV (`document` multipart form field; CSV only) | +| `PUT` | `/v1/contacts/:contactID`| Update name / emails / phone numbers / properties | +| `DELETE` | `/v1/contacts/:contactID`| Delete a contact | + +### 5.1 Create (one or many) + +Request body (array form or `{ "contacts": [...] }`): + +```json +{ + "contacts": [ + { + "name": "Alice Smith", + "emails": ["alice@example.com"], + "phone_numbers": ["+18005550199", "+18005550100"], + "properties": { "company": "Acme", "role": "CTO" } + } + ] +} +``` + +- Per-item validation with row-indexed error messages (mirroring bulk-message + validation style using `url.Values`). +- Batch capped at <= 1000 contacts. +- Cache invalidated **once** after the batch commits. + +### 5.2 CSV import (CSV only) + +- Reuses the bulk-message CSV parsing pattern (`csvutil`; <= 500 KB; <= 1000 + rows). **Excel/XLSX is not supported** — only `text/csv` / `.csv` is accepted; + any other file type returns a validation error. +- Columns: `Name, Emails, PhoneNumbers`. `Emails` and `PhoneNumbers` cells hold + multiple values separated by `;` (or `,`). `Properties` is not part of the + flat template. +- Published template: `httpsms-contacts.csv` (same location/convention as the + bulk-message template). + +### 5.3 Validation rules + +- `Name` required and non-empty per contact. +- Each phone number must parse as E.164 (`nyaruka/phonenumbers`); >= 1 required. +- Each email (if present) must be a valid email. +- `properties` keys/values are free-form strings. + +## 6. Thread name resolution + caching + +### 6.1 Contact map cache + +- A per-user map `phone_number -> *Contact` is built from **all** the user's + contacts. On phone-number collisions, the **most recently updated** contact + wins (sort by `UpdatedAt` ascending while building so later writes overwrite). +- Serialized as JSON in `cache.Cache` under key `contacts.map.`, + TTL ~24h. +- `cache.Cache` exposes only `Get`/`Set` (no delete), so **invalidation = + overwrite** the key with an empty marker after any contact mutation; the next + request that needs it lazily rebuilds from the DB. + +### 6.2 `GetThreads` flow + +1. Load threads (existing single query, unchanged). +2. **If** the request's `Contacts` flag is true: fetch the user's contact map + (cache hit → 0 DB queries; miss → one query to rebuild), then attach + `ContactDetails` to each thread in memory by looking up `thread.contact`. +3. If the flag is false/absent: skip resolution entirely (no cache/DB work); + behavior identical to today. + +Cost profile: + +- Threads read: **1 DB query** (unchanged). +- Contact resolution: usually a **cache hit → 0 DB queries**; DB touched only on + cache miss or right after a contact write. +- Contact **name change**: single-row `UPDATE` + cache invalidation. + **Zero thread writes / zero propagation.** + +### 6.3 Opt-in filter + +- `GET /v1/message-threads` gains an optional query param `?contacts=true` + bound onto `requests.MessageThreadIndex` (`Contacts bool`, default `false`). +- Threaded through `MessageThreadGetParams` to the service, controlling whether + step 2 above runs. + +## 7. Frontend (Nuxt 4 / Pinia) + +- **`useContactsStore`** (`web/app/stores/contacts.ts`): `loadContacts`, + `saveContacts` (array), `updateContact`, `deleteContact`, `uploadCsv`. +- **Contacts page** (`web/app/pages/contacts/index.vue`) — layout modeled on the + provided reference (Plunk-style): + - **Header row**: large `Contacts` title (`text-display-large`, no glowing + gradient) with a subtitle like `Manage your contacts. {total} total`, and, + aligned top-right, an outlined **Import CSV** button and a filled primary + **Add Contact** button. + - **Search bar** below the header filtering by name / email / phone number + (drives the `?query=` API param, debounced). + - **Data table** (`VDataTable`) with columns: `Name`, `Phone Numbers`, + `Emails`, `Created`, `Updated`, and a right-aligned **Actions** column with + per-row **edit** (pencil, `mdiPencil`) and **delete** (trash, + `mdiDelete`) icon buttons. + - Timestamps rendered relatively (e.g. "31 minutes ago") via `useFilters()`. + - Pagination for large lists. + - **Modals** (all `VDialog` with `opacity="0.9"`, Close button + `color="warning"`): + - **Add / Edit Contact** dialog — form for `Name`, repeatable + `PhoneNumbers`, repeatable `Emails`, and free-form `Properties` + (key/value rows). Same dialog component for create and edit. + - **Delete Contact** confirmation dialog. + - **Import CSV** dialog — file input accepting `.csv` only, a link to the + `httpsms-contacts.csv` template, and inline row-indexed error display. + - Hyperlinks use `text-decoration-none hover:text-decoration-underline`. +- **Threads UI**: + - `threads` store `loadThreads` passes `contacts: true`. + - `MessageThread.vue` title and `MessageThreadHeader.vue` render + `thread.contact_details?.name ?? formatPhoneNumber(thread.contact)`; the + avatar uses the resolved name's first letter when present. + - Filter helpers pulled from `useFilters()` in ` + + +``` + +- [ ] **Step 2: Lint the page** + +Run: `cd web && pnpm lint:js && pnpm lint:prettier` +Expected: no errors for `app/pages/contacts/index.vue`. If Prettier reports formatting, run `pnpm lintfix` and re-check. + +- [ ] **Step 3: Build the site to confirm it compiles** + +Run: `cd web && pnpm generate` +Expected: build completes without type errors (the `/contacts` route is emitted). + +- [ ] **Step 4: Commit** + +```bash +git add web/app/pages/contacts/index.vue +git commit -m "feat(web): add contacts page with add/edit/delete/import dialogs" +``` + +--- + +### Task 13: Show contact names on threads + Contacts nav link + +**Files:** +- Modify: `web/app/stores/threads.ts` (send `contacts: true`) +- Modify: `web/app/components/MessageThread.vue` (list title + avatar initial) +- Modify: `web/app/pages/threads/[id]/index.vue` (toolbar title) +- Modify: `web/app/components/MessageThreadHeader.vue` (Contacts nav item + icon import) + +**Interfaces:** +- Consumes: `EntitiesMessageThread.contact_details?: EntitiesContact` (Task 10), the `/contacts` route (Task 12). +- Produces: no new exports — UI wiring only. + +- [ ] **Step 1: Request contact resolution when loading threads** + +In `web/app/stores/threads.ts`, add `contacts: true` to the `loadThreads` request params: + +```ts + const response = await apiFetch<{ data: EntitiesMessageThread[] }>( + '/v1/message-threads', + { + params: { + owner: phonesStore.owner ?? phonesStore.phones[0]?.phone_number, + limit: 100, + is_archived: archivedThreads.value, + contacts: true, + }, + }, + ) +``` + +- [ ] **Step 2: Show the contact name in the threads list** + +In `web/app/components/MessageThread.vue`, replace the list-item title expression: + +```vue + {{ + thread.contact_details?.name ?? formatPhoneNumber(thread.contact) + }} +``` + +And update the avatar initial to prefer the contact name (replace the existing ` Search Messages + + + Contacts + Settings diff --git a/web/app/composables/useFilters.ts b/web/app/composables/useFilters.ts index c84157ea5..2b54bbb43 100644 --- a/web/app/composables/useFilters.ts +++ b/web/app/composables/useFilters.ts @@ -7,6 +7,8 @@ import { formatBillingPeriod, formatBillingPeriodDateOrdinal, humanizeTime, + humanizeTimeShort, + startsWithLetter, } from '../utils/filters' import { capitalize } from '../utils/capitalize' @@ -18,8 +20,10 @@ export function useFilters() { formatMoney, formatDecimal, formatBillingPeriod, + humanizeTimeShort, formatBillingPeriodDateOrdinal, humanizeTime, + startsWithLetter, capitalize, } } diff --git a/web/app/pages/billing/index.vue b/web/app/pages/billing/index.vue index 555bd129c..c5af811e3 100644 --- a/web/app/pages/billing/index.vue +++ b/web/app/pages/billing/index.vue @@ -27,7 +27,7 @@ useHead({ }) const config = useRuntimeConfig() -const { lgAndUp } = useDisplay() +const { lgAndUp } = useVDisplay() const authStore = useAuthStore() const billingStore = useBillingStore() const notificationsStore = useNotificationsStore() diff --git a/web/app/pages/blog/end-to-end-encryption-to-sms-messages.vue b/web/app/pages/blog/end-to-end-encryption-to-sms-messages.vue index 5e7e0a6f9..bda47bca9 100644 --- a/web/app/pages/blog/end-to-end-encryption-to-sms-messages.vue +++ b/web/app/pages/blog/end-to-end-encryption-to-sms-messages.vue @@ -1,10 +1,8 @@ + + diff --git a/web/app/pages/heartbeats/[id].vue b/web/app/pages/heartbeats/[id].vue index 8a05d6944..3ef5d212c 100644 --- a/web/app/pages/heartbeats/[id].vue +++ b/web/app/pages/heartbeats/[id].vue @@ -17,7 +17,7 @@ useHead({ }) const route = useRoute() -const { mdAndDown, mdAndUp, lgAndUp } = useDisplay() +const { mdAndDown, mdAndUp, lgAndUp } = useVDisplay() const authStore = useAuthStore() const phonesStore = usePhonesStore() const { formatPhoneNumber, formatTimestamp } = useFilters() diff --git a/web/app/pages/index.vue b/web/app/pages/index.vue index 281745691..be30493f7 100644 --- a/web/app/pages/index.vue +++ b/web/app/pages/index.vue @@ -43,7 +43,7 @@ useSeoMeta({ }) const config = useRuntimeConfig() -const { lgAndUp, mdAndUp, mdAndDown, md, smAndDown, xl } = useDisplay() +const { lgAndUp, mdAndUp, mdAndDown, md, smAndDown, xl } = useVDisplay() const selectedTab = ref('javascript') const yearlyPricing = ref(false) diff --git a/web/app/pages/messages/index.vue b/web/app/pages/messages/index.vue index 0a6d53a99..3dd1a544a 100644 --- a/web/app/pages/messages/index.vue +++ b/web/app/pages/messages/index.vue @@ -17,7 +17,7 @@ useHead({ }) const router = useRouter() -const { mdAndDown, mdAndUp } = useDisplay() +const { mdAndDown, mdAndUp } = useVDisplay() const notificationsStore = useNotificationsStore() const phonesStore = usePhonesStore() const { useApi } = useApiComposable() diff --git a/web/app/pages/phone-api-keys/index.vue b/web/app/pages/phone-api-keys/index.vue index 27804cbde..df13c5a67 100644 --- a/web/app/pages/phone-api-keys/index.vue +++ b/web/app/pages/phone-api-keys/index.vue @@ -16,7 +16,7 @@ useHead({ }) const config = useRuntimeConfig() -const { lgAndUp } = useDisplay() +const { lgAndUp } = useVDisplay() const authStore = useAuthStore() const appStore = useAppStore() const phonesStore = usePhonesStore() diff --git a/web/app/pages/search-messages/index.vue b/web/app/pages/search-messages/index.vue index 925800d75..0e5419f71 100644 --- a/web/app/pages/search-messages/index.vue +++ b/web/app/pages/search-messages/index.vue @@ -42,7 +42,7 @@ useHead({ const route = useRoute() const config = useRuntimeConfig() -const { mdAndUp, smAndDown, lgAndUp } = useDisplay() +const { mdAndUp, smAndDown, lgAndUp } = useVDisplay() const messagesStore = useMessagesStore() const phonesStore = usePhonesStore() const authStore = useAuthStore() diff --git a/web/app/pages/settings/index.vue b/web/app/pages/settings/index.vue index 1f381416b..63a8c2975 100644 --- a/web/app/pages/settings/index.vue +++ b/web/app/pages/settings/index.vue @@ -42,7 +42,7 @@ useHead({ const config = useRuntimeConfig() const route = useRoute() const router = useRouter() -const { mdAndDown, mdAndUp, lgAndUp, xlAndUp, smAndUp } = useDisplay() +const { mdAndDown, mdAndUp, lgAndUp, xlAndUp, smAndUp } = useVDisplay() const authStore = useAuthStore() const phonesStore = usePhonesStore() const billingStore = useBillingStore() diff --git a/web/app/pages/threads/[id]/index.vue b/web/app/pages/threads/[id]/index.vue index 73632a64c..f933973dd 100644 --- a/web/app/pages/threads/[id]/index.vue +++ b/web/app/pages/threads/[id]/index.vue @@ -14,12 +14,14 @@ import { mdiAccount, mdiRefresh, mdiContentCopy, + mdiAccountPlus, + mdiSquareEditOutline, } from '@mdi/js' import Pusher from 'pusher-js' import type { Channel } from 'pusher-js' import { isValidPhoneNumber } from 'libphonenumber-js' +import { storeToRefs } from 'pinia' import type { EntitiesMessage } from '~~/shared/types/api' -import { startsWithLetter } from '~/utils/filters' definePageMeta({ middleware: ['auth'], @@ -32,13 +34,14 @@ useHead({ const route = useRoute() const router = useRouter() const config = useRuntimeConfig() -const { lgAndUp, mdAndDown, mdAndUp } = useDisplay() -const { formatPhoneNumber } = useFilters() +const { lgAndUp, mdAndDown, mdAndUp } = useVDisplay() +const { formatPhoneNumber, startsWithLetter } = useFilters() const notificationsStore = useNotificationsStore() const authStore = useAuthStore() const phonesStore = usePhonesStore() const threadsStore = useThreadsStore() const messagesStore = useMessagesStore() +const { currentThread } = storeToRefs(threadsStore) const formMessage = ref('') const submitting = ref(false) @@ -46,6 +49,7 @@ const loadingMessages = ref(false) const hideMessages = ref(true) const messages = ref([]) const selectedMenuItem = ref(-1) +const contactDialog = ref(false) const messageBody = ref(null) const form = ref<{ validate: () => Promise<{ valid: boolean }> } | null>(null) @@ -59,7 +63,7 @@ const formMessageRules = [ let webhookChannel: Channel | null = null const contactIsPhoneNumber = computed(() => { - const thread = threadsStore.currentThread + const thread = currentThread.value if (!thread) return false return isValidPhoneNumber(thread.contact) || !isNaN(Number(thread.contact)) }) @@ -67,7 +71,7 @@ const contactIsPhoneNumber = computed(() => { const messageVisibility = computed(() => hideMessages.value ? 'hidden' : 'visible', ) -const contact = computed(() => threadsStore.currentThread?.contact ?? '') +const contact = computed(() => currentThread.value?.contact ?? '') function isMT(message: EntitiesMessage): boolean { return message.type === 'mobile-terminated' @@ -104,6 +108,18 @@ function formatAttachmentName(url: string): string { return url } +function currentThreadContactTitle(): string { + const thread = currentThread.value + if (!thread) return '' + return ( + thread.contact_details?.name?.trim() || formatPhoneNumber(thread.contact) + ) +} + +async function refreshThreadContact() { + await threadsStore.loadThreads() +} + function scrollToElement() { const el = messageBody.value if (el) { @@ -155,7 +171,7 @@ async function loadData() { async function archiveThread() { await threadsStore.updateThread({ - threadId: threadsStore.currentThread!.id, + threadId: currentThread.value!.id, isArchived: true, }) await router.push('/threads') @@ -163,7 +179,7 @@ async function archiveThread() { async function unArchiveThread() { await threadsStore.updateThread({ - threadId: threadsStore.currentThread!.id, + threadId: currentThread.value!.id, isArchived: false, }) await router.push('/threads') @@ -218,7 +234,7 @@ async function sendMessage(event: KeyboardEvent | Event) { submitting.value = true await messagesStore.sendMessage({ from: phonesStore.owner!, - to: threadsStore.currentThread!.contact, + to: currentThread.value!.contact, content: formMessage.value, sim: 'DEFAULT', }) @@ -267,8 +283,17 @@ onBeforeUnmount(() => { - - {{ formatPhoneNumber(threadsStore.currentThread.contact) }} + + + + +