Skip to content

Commit 3ab6267

Browse files
Thomas StrombergThomas Stromberg
authored andcommitted
Add more logging to usermap failures
1 parent 336fc56 commit 3ab6267

File tree

4 files changed

+211
-27
lines changed

4 files changed

+211
-27
lines changed

.claude/CLAUDE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,7 @@ slacker/
139139
- `reactions:write` - Add and edit emoji reactions
140140
- `team:read` - **View workspace name and domain (required for workspace validation)**
141141
- `users:read` - View people in the workspace
142+
- `users:read.email` - **Access user email addresses (required for GitHub→Slack user mapping)**
142143
143144
### State Management
144145
- Track PR states and transitions for notification logic

internal/bot/bot.go

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -654,7 +654,7 @@ func (c *Coordinator) handlePullRequestEventWithData(ctx context.Context, owner,
654654
dmCtx, dmCancel := context.WithTimeout(context.WithoutCancel(ctx), 15*time.Second)
655655
go func() {
656656
defer dmCancel()
657-
c.sendDMNotifications(dmCtx, workspaceID, owner, repo, prNumber, uniqueUsers, event, prState)
657+
c.sendDMNotifications(dmCtx, workspaceID, owner, repo, prNumber, uniqueUsers, event, prState, checkResult)
658658
}()
659659
} else {
660660
slog.Info("no users blocking PR - no notifications needed",
@@ -682,6 +682,7 @@ func (c *Coordinator) sendDMNotifications(
682682
Number int `json:"number"`
683683
},
684684
prState string,
685+
checkResult *turn.CheckResponse,
685686
) {
686687
slog.Info("starting DM notification batch",
687688
logFieldPR, fmt.Sprintf(prFormatString, owner, repo, prNumber),
@@ -726,6 +727,11 @@ func (c *Coordinator) sendDMNotifications(
726727
State: prState,
727728
HTMLURL: event.PullRequest.HTMLURL,
728729
}
730+
// Add workflow state and next actions if available
731+
if checkResult != nil {
732+
prInfo.WorkflowState = checkResult.Analysis.WorkflowState
733+
prInfo.NextAction = checkResult.Analysis.NextAction
734+
}
729735

730736
err = c.notifier.NotifyUser(ctx, workspaceID, slackUserID, tagInfo.ChannelID, channelName, prInfo)
731737
if err != nil {
@@ -895,7 +901,13 @@ func (c *Coordinator) updateDMMessagesForPR(ctx context.Context, pr prUpdateInfo
895901
}
896902

897903
// Format the DM message (same format as initial send)
898-
prefix := notify.PrefixForState(prState)
904+
// Determine prefix based on workflow state and next actions
905+
var prefix string
906+
if checkResult != nil {
907+
prefix = notify.PrefixForAnalysis(checkResult.Analysis.WorkflowState, checkResult.Analysis.NextAction)
908+
} else {
909+
prefix = notify.PrefixForState(prState)
910+
}
899911
var action string
900912
switch prState {
901913
case "newly_published":
@@ -1232,7 +1244,13 @@ func (c *Coordinator) processPRForChannel(
12321244
// Build what the message SHOULD be based on current PR state
12331245
// Then compare to what it IS - update if different
12341246
if !wasNewlyCreated {
1235-
expectedPrefix := notify.PrefixForState(prState)
1247+
// Determine expected prefix based on workflow state and next actions
1248+
var expectedPrefix string
1249+
if checkResult != nil {
1250+
expectedPrefix = notify.PrefixForAnalysis(checkResult.Analysis.WorkflowState, checkResult.Analysis.NextAction)
1251+
} else {
1252+
expectedPrefix = notify.PrefixForState(prState)
1253+
}
12361254
domain := c.configManager.Domain(owner)
12371255
urlWithState := event.PullRequest.HTMLURL + c.getStateQueryParam(prState)
12381256

@@ -1469,8 +1487,14 @@ func (c *Coordinator) createPRThread(ctx context.Context, channel, owner, repo s
14691487
Number int `json:"number"`
14701488
}, checkResult *turn.CheckResponse,
14711489
) (threadTS string, messageText string, err error) {
1472-
// Get state-based prefix
1473-
prefix := notify.PrefixForState(prState)
1490+
// Get emoji prefix based on workflow state and next actions
1491+
var prefix string
1492+
if checkResult != nil {
1493+
prefix = notify.PrefixForAnalysis(checkResult.Analysis.WorkflowState, checkResult.Analysis.NextAction)
1494+
} else {
1495+
// Fallback to state-based prefix if no checkResult available
1496+
prefix = notify.PrefixForState(prState)
1497+
}
14741498

14751499
// Add state query param to URL for debugging
14761500
urlWithState := pr.HTMLURL + c.getStateQueryParam(prState)

internal/notify/notify.go

Lines changed: 109 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"time"
99

1010
"github.com/codeGROOVE-dev/slacker/internal/slack"
11+
"github.com/codeGROOVE-dev/turnclient/pkg/turn"
1112
)
1213

1314
// Constants for notification defaults.
@@ -77,17 +78,21 @@ func (m *Manager) Run(ctx context.Context) error {
7778

7879
// PRInfo contains the minimal information needed to notify about a PR.
7980
type PRInfo struct {
80-
Owner string
81-
Repo string
82-
Title string
83-
Author string
84-
State string
85-
HTMLURL string
86-
Number int
81+
Owner string
82+
Repo string
83+
Title string
84+
Author string
85+
State string // Deprecated: use WorkflowState and NextAction for emoji determination
86+
HTMLURL string
87+
Number int
88+
WorkflowState string // Workflow state from turnclient Analysis
89+
NextAction map[string]turn.Action // Next actions from turnclient Analysis
8790
}
8891

8992
// PrefixForState returns the emoji prefix for a given PR state.
9093
// Exported for use by bot package to ensure consistent PR state display.
94+
//
95+
// Deprecated: Use PrefixForAnalysis instead to derive emoji from NextAction.
9196
func PrefixForState(prState string) string {
9297
switch prState {
9398
case "newly_published":
@@ -111,6 +116,95 @@ func PrefixForState(prState string) string {
111116
}
112117
}
113118

119+
// PrefixForAction returns the emoji prefix for a given action kind.
120+
func PrefixForAction(action string) string {
121+
switch action {
122+
case "publish_draft":
123+
return ":construction:"
124+
case "fix_tests":
125+
return ":cockroach:"
126+
case "tests_pending":
127+
return ":test_tube:"
128+
case "review", "re_review", "request_reviewers":
129+
return ":hourglass:"
130+
case "resolve_comments", "respond", "review_discussion":
131+
return ":carpentry_saw:"
132+
case "approve":
133+
return ":white_check_mark:"
134+
case "merge":
135+
return ":rocket:"
136+
default:
137+
return ":postal_horn:"
138+
}
139+
}
140+
141+
// actionPriority returns a priority score for action kinds (lower = higher priority).
142+
// Used to determine which emoji to show when multiple actions are pending.
143+
func actionPriority(action string) int {
144+
switch action {
145+
case "publish_draft":
146+
return 1
147+
case "fix_tests":
148+
return 2
149+
case "tests_pending":
150+
return 3
151+
case "fix_conflict":
152+
return 4
153+
case "resolve_comments", "respond", "review_discussion":
154+
return 5
155+
case "review", "re_review", "request_reviewers":
156+
return 6
157+
case "approve":
158+
return 7
159+
case "merge":
160+
return 8
161+
default:
162+
return 99
163+
}
164+
}
165+
166+
// PrimaryAction determines the primary action kind from a NextAction map.
167+
// Returns the highest-priority action (lowest priority score).
168+
func PrimaryAction(nextActions map[string]turn.Action) string {
169+
if len(nextActions) == 0 {
170+
return ""
171+
}
172+
173+
var primaryAction string
174+
minPriority := 999
175+
176+
for _, action := range nextActions {
177+
kind := string(action.Kind)
178+
priority := actionPriority(kind)
179+
if priority < minPriority {
180+
minPriority = priority
181+
primaryAction = kind
182+
}
183+
}
184+
185+
return primaryAction
186+
}
187+
188+
// PrefixForAnalysis returns the emoji prefix based on workflow state and next actions.
189+
// This is the primary function for determining PR emoji - it handles the logic:
190+
// 1. If workflow_state == "newly_published" → ":new:"
191+
// 2. Otherwise → emoji based on primary next_action
192+
func PrefixForAnalysis(workflowState string, nextActions map[string]turn.Action) string {
193+
// Special case: newly published PRs always show :new:
194+
if workflowState == "newly_published" {
195+
return ":new:"
196+
}
197+
198+
// Determine primary action and return corresponding emoji
199+
primaryAction := PrimaryAction(nextActions)
200+
if primaryAction != "" {
201+
return PrefixForAction(primaryAction)
202+
}
203+
204+
// Fallback if no actions
205+
return ":postal_horn:"
206+
}
207+
114208
// NotifyUser sends a smart notification to a user about a PR using the configured logic.
115209
// Implements delayed DM logic: if user was tagged in channel, delay by configured time.
116210
// If user is not in channel where tagged, send DM immediately.
@@ -245,8 +339,14 @@ func (m *Manager) NotifyUser(ctx context.Context, workspaceID, userID, channelID
245339
}
246340

247341
// Format notification message using same style as channel messages
248-
// Use state-based emoji prefix like channel messages do
249-
prefix := PrefixForState(pr.State)
342+
// Determine emoji prefix based on workflow state and next actions
343+
var prefix string
344+
if pr.WorkflowState != "" {
345+
prefix = PrefixForAnalysis(pr.WorkflowState, pr.NextAction)
346+
} else {
347+
// Fallback to state if workflow state not available
348+
prefix = PrefixForState(pr.State)
349+
}
250350

251351
// Format: :emoji: Title <url|repo#123> · author → action
252352
var action string

internal/usermapping/usermapping.go

Lines changed: 72 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -138,10 +138,16 @@ func (s *Service) SlackHandle(ctx context.Context, githubUsername, organization,
138138
// Extracted from SlackHandle to work with singleflight pattern.
139139
func (s *Service) doLookup(ctx context.Context, githubUsername, organization, domain string) (string, error) {
140140
// Get emails for GitHub user with organization context
141+
slog.Info("starting email lookup for GitHub user",
142+
"github_user", githubUsername,
143+
"organization", organization,
144+
"domain", domain)
145+
141146
result, err := s.githubLookup.Lookup(ctx, githubUsername, organization)
142147
if err != nil {
143148
slog.Warn("failed to get emails for GitHub user",
144149
"github_user", githubUsername,
150+
"organization", organization,
145151
"error", err)
146152
return "", err
147153
}
@@ -154,10 +160,17 @@ func (s *Service) doLookup(ctx context.Context, githubUsername, organization, do
154160
emails[i] = addr.Email
155161
}
156162

157-
slog.Debug("found emails for GitHub user",
163+
slog.Info("found emails for GitHub user via lookup",
158164
"github_user", githubUsername,
159165
"email_count", len(emails),
160-
"emails", emails)
166+
"emails", emails,
167+
"methods", func() []string {
168+
methods := make([]string, len(result.Addresses))
169+
for i, addr := range result.Addresses {
170+
methods[i] = addr.Methods[0] // Show first method
171+
}
172+
return methods
173+
}())
161174

162175
// Try finding Slack matches with all emails first
163176
matches := s.findSlackMatches(ctx, githubUsername, emails)
@@ -208,28 +221,38 @@ func (s *Service) doLookup(ctx context.Context, githubUsername, organization, do
208221
}
209222
}
210223
// Finally, try intelligent guessing
211-
slog.Debug("trying intelligent email guessing",
224+
slog.Info("starting intelligent email guessing",
212225
"github_user", githubUsername,
213-
"domain", domain)
226+
"organization", organization,
227+
"domain", domain,
228+
"found_addresses_count", len(result.Addresses))
214229

215230
guessResult, err := s.githubLookup.Guess(ctx, githubUsername, organization, ghmailto.GuessOptions{
216231
Domain: domain,
217232
})
218233
if err != nil {
219234
slog.Warn("email guessing failed",
220235
"github_user", githubUsername,
236+
"organization", organization,
221237
"domain", domain,
222238
"error", err)
223239
} else if len(guessResult.Guesses) > 0 {
224240
guessedEmails := make([]string, len(guessResult.Guesses))
241+
confidences := make([]int, len(guessResult.Guesses))
242+
patterns := make([]string, len(guessResult.Guesses))
225243
for i, addr := range guessResult.Guesses {
226244
guessedEmails[i] = addr.Email
245+
confidences[i] = addr.Confidence
246+
patterns[i] = addr.Pattern
227247
}
228248

229-
slog.Debug("generated email guesses",
249+
slog.Info("generated email guesses for Slack lookup",
230250
"github_user", githubUsername,
231251
"domain", domain,
232-
"guesses", guessedEmails)
252+
"guess_count", len(guessedEmails),
253+
"guesses", guessedEmails,
254+
"confidences", confidences,
255+
"patterns", patterns)
233256

234257
matches := s.findSlackMatches(ctx, githubUsername, guessedEmails)
235258
if len(matches) > 0 {
@@ -243,13 +266,25 @@ func (s *Service) doLookup(ctx context.Context, githubUsername, organization, do
243266
s.cacheMapping(bestMatch)
244267
return bestMatch.SlackUserID, nil
245268
}
269+
slog.Warn("email guesses generated but no Slack matches found",
270+
"github_user", githubUsername,
271+
"domain", domain,
272+
"guesses_tried", len(guessedEmails),
273+
"guesses", guessedEmails)
274+
} else {
275+
slog.Warn("email guessing completed but no guesses generated",
276+
"github_user", githubUsername,
277+
"domain", domain)
246278
}
247279
}
248280

249281
// No matches found through any method
250-
slog.Info("no Slack mapping found for GitHub user",
282+
slog.Warn("no Slack mapping found for GitHub user after exhausting all methods",
251283
"github_user", githubUsername,
284+
"organization", organization,
285+
"domain", domain,
252286
"tried_direct_emails", len(emails) > 0,
287+
"direct_emails_tried", emails,
253288
"tried_domain_filtering", domain != "",
254289
"tried_guessing", domain != "")
255290

@@ -387,15 +422,31 @@ func (s *Service) cacheMapping(mapping *UserMapping) {
387422
func (s *Service) findSlackMatches(ctx context.Context, githubUsername string, emails []string) []*UserMapping {
388423
var matches []*UserMapping
389424

425+
slog.Info("starting Slack user lookup phase",
426+
"github_user", githubUsername,
427+
"email_count", len(emails),
428+
"emails", emails)
429+
390430
// Search for each email in Slack user directory
391-
for _, email := range emails {
431+
for i, email := range emails {
392432
// Normalize email to lowercase for consistent matching
393433
normalizedEmail := strings.ToLower(email)
434+
slog.Info("attempting Slack API lookup",
435+
"github_user", githubUsername,
436+
"attempt", i+1,
437+
"of", len(emails),
438+
"original_email", email,
439+
"normalized_email", normalizedEmail)
440+
394441
user, err := s.slackClient.GetUserByEmailContext(ctx, normalizedEmail)
395442
if err != nil {
396-
slog.Debug("no Slack user found for email",
443+
// Log detailed error info to help debug Slack API issues
444+
slog.Warn("Slack API lookup failed for email",
445+
"github_user", githubUsername,
397446
"email", email,
398-
"error", err)
447+
"normalized_email", normalizedEmail,
448+
"error_type", fmt.Sprintf("%T", err),
449+
"error_message", err.Error())
399450
continue
400451
}
401452

@@ -420,12 +471,20 @@ func (s *Service) findSlackMatches(ctx context.Context, githubUsername string, e
420471

421472
matches = append(matches, mapping)
422473

423-
slog.Debug("found Slack user match",
474+
slog.Info("found Slack user match",
475+
"github_user", githubUsername,
424476
"email", email,
425-
"slack_user", user.Name,
426-
"confidence", confidence)
477+
"slack_user_id", user.ID,
478+
"slack_user_name", user.Name,
479+
"confidence", confidence,
480+
"is_primary_email", user.Profile.Email == email)
427481
}
428482

483+
slog.Info("Slack user lookup phase complete",
484+
"github_user", githubUsername,
485+
"emails_tried", len(emails),
486+
"matches_found", len(matches))
487+
429488
return matches
430489
}
431490

0 commit comments

Comments
 (0)