Coverage for src / sentry_tool / commands / events.py: 91.36%

81 statements  

« prev     ^ index     » next       coverage.py v7.13.2, created at 2026-02-15 10:53 -0500

1"""Event-related commands.""" 

2 

3from typing import Annotated 

4 

5import typer 

6from rich.console import Console 

7 

8from sentry_tool.output import ( 

9 Column, 

10 OutputFormat, 

11 print_event_context, 

12 print_exception_entry, 

13 render, 

14 render_event_basic_info, 

15) 

16from sentry_tool.services import resolve_issue_to_numeric 

17from sentry_tool.utils import api, get_config 

18 

19app = typer.Typer(help="Event management commands") 

20 

21 

22@app.command("event") 

23def show_event( 

24 issue_id: Annotated[str, typer.Argument(help="Issue ID (numeric or short ID)")], 

25 event_id: Annotated[ 

26 str | None, typer.Option("--event", "-e", help="Specific event ID (default: latest)") 

27 ] = None, 

28 format: Annotated[ 

29 OutputFormat, 

30 typer.Option("--format", "-f", help="Output format"), 

31 ] = OutputFormat.table, 

32 context: Annotated[ 

33 bool, typer.Option("--context", "-c", help="Show only context/stacktrace") 

34 ] = False, 

35) -> None: 

36 """ 

37 Show event details for an issue. 

38 

39 By default shows the latest event. Use --event to specify a particular event. 

40 

41 Examples: 

42 sentry-tool event 24 # Latest event for issue 24 

43 sentry-tool event OTEL-COLLECTOR-Q # Latest event by short ID 

44 sentry-tool event 24 -c # Show just context/stacktrace 

45 sentry-tool event 24 --format json # Full JSON output 

46 """ 

47 config = get_config() 

48 

49 numeric_id, short_id = resolve_issue_to_numeric(config, issue_id) 

50 

51 if event_id: 

52 event = api( 

53 f"/organizations/{config['org']}/issues/{numeric_id}/events/{event_id}/", 

54 token=config["auth_token"], 

55 base_url=config["url"], 

56 ) 

57 else: 

58 event = api( 

59 f"/organizations/{config['org']}/issues/{numeric_id}/events/latest/", 

60 token=config["auth_token"], 

61 base_url=config["url"], 

62 ) 

63 

64 if format == OutputFormat.json: 

65 render([event], format) 

66 return 

67 

68 console = Console() 

69 

70 render_event_basic_info(console, event, short_id) 

71 

72 ctx = event.get("context", {}) 

73 if ctx: 

74 print_event_context(console, ctx) 

75 

76 entries = event.get("entries", []) 

77 for entry in entries: 

78 entry_type = entry.get("type", "") 

79 data = entry.get("data", {}) 

80 

81 if entry_type == "message" and not context: 

82 formatted = data.get("formatted", "") 

83 if formatted: 

84 console.print(f"\n[bold]Formatted Message:[/bold]\n {formatted}") 

85 

86 elif entry_type == "exception": 

87 print_exception_entry(console, data) 

88 

89 console.print() 

90 

91 

92@app.command("events") 

93def list_events( 

94 issue_id: Annotated[str, typer.Argument(help="Issue ID (numeric or short ID)")], 

95 max_rows: Annotated[int, typer.Option("--max", "-n", help="Maximum events to show")] = 10, 

96 format: Annotated[ 

97 OutputFormat, 

98 typer.Option("--format", "-f", help="Output format"), 

99 ] = OutputFormat.table, 

100) -> None: 

101 """ 

102 List recent events for an issue. 

103 

104 Examples: 

105 sentry-tool events 24 

106 sentry-tool events OTEL-COLLECTOR-Q -n 5 

107 sentry-tool events 24 --format json 

108 """ 

109 config = get_config() 

110 

111 numeric_id, _short_id = resolve_issue_to_numeric(config, issue_id) 

112 

113 events = api( 

114 f"/organizations/{config['org']}/issues/{numeric_id}/events/", 

115 token=config["auth_token"], 

116 base_url=config["url"], 

117 ) 

118 

119 if not events: 

120 Console().print("No events found") 

121 return 

122 

123 events = events[:max_rows] 

124 

125 rows = [] 

126 for evt in events: 

127 evt_id = evt.get("eventID", evt.get("id", "")) 

128 date = evt.get("dateCreated", "")[:19] 

129 

130 server = "-" 

131 for tag in evt.get("tags", []): 

132 if tag.get("key") == "server_name": 

133 server = tag.get("value", "-") 

134 break 

135 

136 rows.append({"eventID": evt_id, "date": date, "server": server}) 

137 

138 columns = [ 

139 Column("Event ID", "eventID", style="dim", max_width=36), 

140 Column("Date", "date"), 

141 Column("Server", "server"), 

142 ] 

143 

144 render(rows, format, columns=columns, footer=f"Showing {len(events)} events") 

145 

146 

147@app.command("tags") 

148def show_tags( 

149 issue_id: Annotated[str, typer.Argument(help="Issue ID (numeric or short ID)")], 

150 tag_key: Annotated[ 

151 str | None, typer.Argument(help="Tag key to show values for (e.g., server_name)") 

152 ] = None, 

153 format: Annotated[ 

154 OutputFormat, 

155 typer.Option("--format", "-f", help="Output format"), 

156 ] = OutputFormat.table, 

157) -> None: 

158 """ 

159 Show tag values for an issue. 

160 

161 Without TAG_KEY, lists available tags. With TAG_KEY, shows values for that tag. 

162 

163 Examples: 

164 sentry-tool tags OTEL-COLLECTOR-14 # List available tags 

165 sentry-tool tags OTEL-COLLECTOR-14 server_name # Show affected hosts 

166 sentry-tool tags OTEL-COLLECTOR-14 release # Show affected releases 

167 sentry-tool tags 14 server_name --format json 

168 """ 

169 config = get_config() 

170 

171 numeric_id, _short_id = resolve_issue_to_numeric(config, issue_id) 

172 

173 console = Console() 

174 

175 if tag_key: 

176 tag_data = api( 

177 f"/organizations/{config['org']}/issues/{numeric_id}/tags/{tag_key}/", 

178 token=config["auth_token"], 

179 base_url=config["url"], 

180 ) 

181 

182 top_values = tag_data.get("topValues", []) 

183 if not top_values: 

184 console.print(f"No values found for tag '{tag_key}'") 

185 return 

186 

187 rows = [] 

188 for val in top_values: 

189 name = val.get("value", "")[:30] 

190 count = val.get("count", 0) 

191 pct = val.get("percentage", 0) * 100 

192 rows.append({"value": name, "count": str(count), "percent": f"{pct:.1f}%"}) 

193 

194 columns = [ 

195 Column("Value", "value", max_width=30), 

196 Column("Count", "count", justify="right"), 

197 Column("Percent", "percent", justify="right"), 

198 ] 

199 

200 render( 

201 rows, 

202 format, 

203 columns=columns, 

204 footer=f"Total unique values: {tag_data.get('uniqueValues', 'N/A')}", 

205 ) 

206 else: 

207 issue = api( 

208 f"/organizations/{config['org']}/issues/{numeric_id}/", 

209 token=config["auth_token"], 

210 base_url=config["url"], 

211 ) 

212 tags = issue.get("tags", []) 

213 if not tags: 

214 console.print("No tags found") 

215 return 

216 

217 rows = [ 

218 {"key": tag.get("key", ""), "total": str(tag.get("totalValues", 0))} for tag in tags 

219 ] 

220 

221 columns = [ 

222 Column("Tag Key", "key"), 

223 Column("Unique Values", "total", justify="right"), 

224 ] 

225 

226 render(rows, format, columns=columns)