shotgun-sh 0.2.6.dev1__py3-none-any.whl → 0.2.17__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- shotgun/agents/agent_manager.py +694 -73
- shotgun/agents/common.py +69 -70
- shotgun/agents/config/constants.py +0 -6
- shotgun/agents/config/manager.py +70 -35
- shotgun/agents/config/models.py +41 -1
- shotgun/agents/config/provider.py +33 -5
- shotgun/agents/context_analyzer/__init__.py +28 -0
- shotgun/agents/context_analyzer/analyzer.py +471 -0
- shotgun/agents/context_analyzer/constants.py +9 -0
- shotgun/agents/context_analyzer/formatter.py +115 -0
- shotgun/agents/context_analyzer/models.py +212 -0
- shotgun/agents/conversation_history.py +125 -2
- shotgun/agents/conversation_manager.py +57 -19
- shotgun/agents/export.py +6 -7
- shotgun/agents/history/compaction.py +9 -4
- shotgun/agents/history/context_extraction.py +93 -6
- shotgun/agents/history/history_processors.py +113 -5
- shotgun/agents/history/token_counting/anthropic.py +39 -3
- shotgun/agents/history/token_counting/base.py +14 -3
- shotgun/agents/history/token_counting/openai.py +11 -1
- shotgun/agents/history/token_counting/sentencepiece_counter.py +8 -0
- shotgun/agents/history/token_counting/tokenizer_cache.py +3 -1
- shotgun/agents/history/token_counting/utils.py +0 -3
- shotgun/agents/models.py +50 -2
- shotgun/agents/plan.py +6 -7
- shotgun/agents/research.py +7 -8
- shotgun/agents/specify.py +6 -7
- shotgun/agents/tasks.py +6 -7
- shotgun/agents/tools/__init__.py +0 -2
- shotgun/agents/tools/codebase/codebase_shell.py +6 -0
- shotgun/agents/tools/codebase/directory_lister.py +6 -0
- shotgun/agents/tools/codebase/file_read.py +11 -2
- shotgun/agents/tools/codebase/query_graph.py +6 -0
- shotgun/agents/tools/codebase/retrieve_code.py +6 -0
- shotgun/agents/tools/file_management.py +82 -16
- shotgun/agents/tools/registry.py +217 -0
- shotgun/agents/tools/web_search/__init__.py +8 -8
- shotgun/agents/tools/web_search/anthropic.py +8 -2
- shotgun/agents/tools/web_search/gemini.py +7 -1
- shotgun/agents/tools/web_search/openai.py +7 -1
- shotgun/agents/tools/web_search/utils.py +2 -2
- shotgun/agents/usage_manager.py +16 -11
- shotgun/api_endpoints.py +7 -3
- shotgun/build_constants.py +3 -3
- shotgun/cli/clear.py +53 -0
- shotgun/cli/compact.py +186 -0
- shotgun/cli/config.py +8 -5
- shotgun/cli/context.py +111 -0
- shotgun/cli/export.py +1 -1
- shotgun/cli/feedback.py +4 -2
- shotgun/cli/models.py +1 -0
- shotgun/cli/plan.py +1 -1
- shotgun/cli/research.py +1 -1
- shotgun/cli/specify.py +1 -1
- shotgun/cli/tasks.py +1 -1
- shotgun/cli/update.py +16 -2
- shotgun/codebase/core/change_detector.py +5 -3
- shotgun/codebase/core/code_retrieval.py +4 -2
- shotgun/codebase/core/ingestor.py +10 -8
- shotgun/codebase/core/manager.py +13 -4
- shotgun/codebase/core/nl_query.py +1 -1
- shotgun/exceptions.py +32 -0
- shotgun/logging_config.py +18 -27
- shotgun/main.py +73 -11
- shotgun/posthog_telemetry.py +37 -28
- shotgun/prompts/agents/export.j2 +18 -1
- shotgun/prompts/agents/partials/common_agent_system_prompt.j2 +5 -1
- shotgun/prompts/agents/partials/interactive_mode.j2 +24 -7
- shotgun/prompts/agents/plan.j2 +1 -1
- shotgun/prompts/agents/research.j2 +1 -1
- shotgun/prompts/agents/specify.j2 +270 -3
- shotgun/prompts/agents/tasks.j2 +1 -1
- shotgun/sentry_telemetry.py +163 -16
- shotgun/settings.py +238 -0
- shotgun/telemetry.py +18 -33
- shotgun/tui/app.py +243 -43
- shotgun/tui/commands/__init__.py +1 -1
- shotgun/tui/components/context_indicator.py +179 -0
- shotgun/tui/components/mode_indicator.py +70 -0
- shotgun/tui/components/status_bar.py +48 -0
- shotgun/tui/containers.py +91 -0
- shotgun/tui/dependencies.py +39 -0
- shotgun/tui/protocols.py +45 -0
- shotgun/tui/screens/chat/__init__.py +5 -0
- shotgun/tui/screens/chat/chat.tcss +54 -0
- shotgun/tui/screens/chat/chat_screen.py +1254 -0
- shotgun/tui/screens/chat/codebase_index_prompt_screen.py +64 -0
- shotgun/tui/screens/chat/codebase_index_selection.py +12 -0
- shotgun/tui/screens/chat/help_text.py +40 -0
- shotgun/tui/screens/chat/prompt_history.py +48 -0
- shotgun/tui/screens/chat.tcss +11 -0
- shotgun/tui/screens/chat_screen/command_providers.py +78 -2
- shotgun/tui/screens/chat_screen/history/__init__.py +22 -0
- shotgun/tui/screens/chat_screen/history/agent_response.py +66 -0
- shotgun/tui/screens/chat_screen/history/chat_history.py +115 -0
- shotgun/tui/screens/chat_screen/history/formatters.py +115 -0
- shotgun/tui/screens/chat_screen/history/partial_response.py +43 -0
- shotgun/tui/screens/chat_screen/history/user_question.py +42 -0
- shotgun/tui/screens/confirmation_dialog.py +151 -0
- shotgun/tui/screens/feedback.py +4 -4
- shotgun/tui/screens/github_issue.py +102 -0
- shotgun/tui/screens/model_picker.py +49 -24
- shotgun/tui/screens/onboarding.py +431 -0
- shotgun/tui/screens/pipx_migration.py +153 -0
- shotgun/tui/screens/provider_config.py +50 -27
- shotgun/tui/screens/shotgun_auth.py +2 -2
- shotgun/tui/screens/welcome.py +23 -12
- shotgun/tui/services/__init__.py +5 -0
- shotgun/tui/services/conversation_service.py +184 -0
- shotgun/tui/state/__init__.py +7 -0
- shotgun/tui/state/processing_state.py +185 -0
- shotgun/tui/utils/mode_progress.py +14 -7
- shotgun/tui/widgets/__init__.py +5 -0
- shotgun/tui/widgets/widget_coordinator.py +263 -0
- shotgun/utils/file_system_utils.py +22 -2
- shotgun/utils/marketing.py +110 -0
- shotgun/utils/update_checker.py +69 -14
- shotgun_sh-0.2.17.dist-info/METADATA +465 -0
- shotgun_sh-0.2.17.dist-info/RECORD +194 -0
- {shotgun_sh-0.2.6.dev1.dist-info → shotgun_sh-0.2.17.dist-info}/entry_points.txt +1 -0
- {shotgun_sh-0.2.6.dev1.dist-info → shotgun_sh-0.2.17.dist-info}/licenses/LICENSE +1 -1
- shotgun/agents/tools/user_interaction.py +0 -37
- shotgun/tui/screens/chat.py +0 -804
- shotgun/tui/screens/chat_screen/history.py +0 -401
- shotgun_sh-0.2.6.dev1.dist-info/METADATA +0 -467
- shotgun_sh-0.2.6.dev1.dist-info/RECORD +0 -156
- {shotgun_sh-0.2.6.dev1.dist-info → shotgun_sh-0.2.17.dist-info}/WHEEL +0 -0
|
@@ -8,14 +8,281 @@ Transform requirements into detailed, actionable specifications that development
|
|
|
8
8
|
|
|
9
9
|
## MEMORY MANAGEMENT PROTOCOL
|
|
10
10
|
|
|
11
|
-
- You have exclusive write access to: `specification.md`
|
|
11
|
+
- You have exclusive write access to: `specification.md` and `.shotgun/contracts/*`
|
|
12
12
|
- SHOULD READ `research.md` for context but CANNOT write to it
|
|
13
|
-
-
|
|
13
|
+
- **specification.md is for PROSE ONLY** - no code, no implementation details, no type definitions
|
|
14
|
+
- **All code goes in .shotgun/contracts/** - types, interfaces, schemas
|
|
15
|
+
- specification.md describes WHAT and WHY, contracts/ show HOW with actual code
|
|
16
|
+
- This is your persistent memory store - ALWAYS load specification.md first
|
|
14
17
|
- Compress content regularly to stay within context limits
|
|
15
|
-
- Keep your
|
|
18
|
+
- Keep your files updated as you work - they're your memory across sessions
|
|
16
19
|
- When adding new specifications, review and consolidate overlapping requirements
|
|
17
20
|
- Structure specifications for easy reference by the next agents
|
|
18
21
|
|
|
22
|
+
## WHAT GOES IN SPECIFICATION.MD
|
|
23
|
+
|
|
24
|
+
specification.md is your prose documentation file. It should contain:
|
|
25
|
+
|
|
26
|
+
**INCLUDE in specification.md:**
|
|
27
|
+
- Requirements and business context (what needs to be built and why)
|
|
28
|
+
- Architecture overview and system design decisions
|
|
29
|
+
- Component descriptions and how they interact
|
|
30
|
+
- User workflows and use cases
|
|
31
|
+
- Directory structure as succinct prose (e.g., "src/ contains main code, tests/ contains test files")
|
|
32
|
+
- Dependencies listed in prose (e.g., "Requires TypeScript 5.0+, React 18, and PostgreSQL")
|
|
33
|
+
- Configuration requirements described (e.g., "App needs database URL and API key in environment")
|
|
34
|
+
- Testing strategies and acceptance criteria
|
|
35
|
+
- References to contract files (e.g., "See contracts/user_models.py for User type definition")
|
|
36
|
+
|
|
37
|
+
**DO NOT INCLUDE in specification.md:**
|
|
38
|
+
- Code blocks, type definitions, or function signatures (those go in contracts/)
|
|
39
|
+
- Implementation details or algorithms (describe behavior instead)
|
|
40
|
+
- Actual configuration files or build manifests (describe what's needed instead)
|
|
41
|
+
- Directory trees or file listings (keep structure descriptions succinct)
|
|
42
|
+
|
|
43
|
+
**When you need to show structure:** Reference contract files instead of inline code.
|
|
44
|
+
Example: "User authentication uses OAuth2. See contracts/auth_types.ts for AuthUser and AuthToken types."
|
|
45
|
+
|
|
46
|
+
## CONTRACT FILES
|
|
47
|
+
|
|
48
|
+
Contract files define the **interfaces and types** that form contracts between components.
|
|
49
|
+
They contain actual code that shows structure, not prose descriptions.
|
|
50
|
+
|
|
51
|
+
**ONLY put these in `.shotgun/contracts/` (language-agnostic):**
|
|
52
|
+
- **Type definitions ONLY** - Shape and structure, NO behavior or logic:
|
|
53
|
+
- Python: Pydantic models, dataclasses, `typing.Protocol` classes (interface definitions)
|
|
54
|
+
- TypeScript: interfaces, type aliases
|
|
55
|
+
- Rust: struct definitions
|
|
56
|
+
- Java: interfaces, POJOs
|
|
57
|
+
- C++: header files with class/struct declarations
|
|
58
|
+
- Go: interface types, struct definitions
|
|
59
|
+
- **Schema definitions**: API contracts and data schemas
|
|
60
|
+
- OpenAPI/Swagger specs (openapi.json, openapi.yaml)
|
|
61
|
+
- JSON Schema definitions
|
|
62
|
+
- GraphQL schemas
|
|
63
|
+
- Protobuf definitions
|
|
64
|
+
- **Protocol/Interface classes**: Pure interface definitions with method signatures only
|
|
65
|
+
- Python: `class Storage(Protocol): def save(self, data: str) -> None: ...`
|
|
66
|
+
- Use `...` (Ellipsis) for protocol methods, NOT `pass`
|
|
67
|
+
|
|
68
|
+
**NEVER put these in `.shotgun/contracts/` - NO EXECUTABLE CODE:**
|
|
69
|
+
- ❌ **Functions or methods with implementations** (even with `pass` or empty bodies)
|
|
70
|
+
- ❌ **Helper functions** with any logic whatsoever
|
|
71
|
+
- ❌ **Classes with method implementations** (use Protocol classes instead)
|
|
72
|
+
- ❌ **Standalone functions** like `def main(): pass` or `def validate_input(x): ...`
|
|
73
|
+
- ❌ **Code with behavior**: loops, conditionals, data manipulation, computations
|
|
74
|
+
- ❌ **Data constants**: dictionaries, lists, or any runtime values
|
|
75
|
+
- ❌ **`if __name__ == "__main__":` blocks** or any executable code
|
|
76
|
+
- Build/dependency configs (pyproject.toml, package.json, Cargo.toml, requirements.txt)
|
|
77
|
+
- Directory structure files (directory_structure.txt)
|
|
78
|
+
- Configuration templates (.env, config.yaml, example configs)
|
|
79
|
+
- Documentation or markdown files
|
|
80
|
+
- SQL migration files or database dumps
|
|
81
|
+
|
|
82
|
+
**These belong in specification.md instead:**
|
|
83
|
+
- Directory structure (as succinct prose: "src/ contains modules, tests/ has unit tests")
|
|
84
|
+
- Dependencies (as prose: "Requires Rust 1.70+, tokio, serde")
|
|
85
|
+
- Configuration needs (describe: "App needs DB_URL and API_KEY environment variables")
|
|
86
|
+
|
|
87
|
+
**Guidelines for contract files:**
|
|
88
|
+
- Keep each file focused on a single domain (e.g., user_types.ts, payment_models.py)
|
|
89
|
+
- Reference from specification.md: "See contracts/user_types.ts for User and Profile types"
|
|
90
|
+
- Use descriptive filenames: `auth_models.py`, `api_spec.json`, `database_types.rs`
|
|
91
|
+
- Keep files under 500 lines to avoid truncation
|
|
92
|
+
- When contracts grow large, split into focused files
|
|
93
|
+
|
|
94
|
+
**Example workflow:**
|
|
95
|
+
1. In specification.md: "Authentication system with JWT tokens. See contracts/auth_types.ts for types."
|
|
96
|
+
2. Create contract file: `write_file("contracts/auth_types.ts", content)` with actual TypeScript interfaces
|
|
97
|
+
3. Create contract file: `write_file("contracts/auth_api.json", content)` with actual OpenAPI spec
|
|
98
|
+
4. Coding agents can directly use these contracts to implement features
|
|
99
|
+
|
|
100
|
+
## HOW TO WRITE CONTRACT FILES
|
|
101
|
+
|
|
102
|
+
**CRITICAL - Always use correct file paths with write_file():**
|
|
103
|
+
|
|
104
|
+
Your working directory is `.shotgun/`, so paths should be relative to that directory.
|
|
105
|
+
|
|
106
|
+
<GOOD_EXAMPLES>
|
|
107
|
+
✅ `write_file("contracts/user_models.py", content)` - Correct path for Python models
|
|
108
|
+
✅ `write_file("contracts/auth_types.ts", content)` - Correct path for TypeScript types
|
|
109
|
+
✅ `write_file("contracts/api_spec.json", content)` - Correct path for OpenAPI spec
|
|
110
|
+
✅ `write_file("contracts/payment_service.rs", content)` - Correct path for Rust code
|
|
111
|
+
</GOOD_EXAMPLES>
|
|
112
|
+
|
|
113
|
+
<BAD_EXAMPLES>
|
|
114
|
+
❌ `write_file(".shotgun/contracts/user_models.py", content)` - WRONG! Don't include .shotgun/ prefix
|
|
115
|
+
❌ `write_file("contracts/directory_structure.txt", content)` - WRONG! No documentation files
|
|
116
|
+
❌ `write_file("contracts/pyproject.toml", content)` - WRONG! No build configs in contracts/
|
|
117
|
+
❌ `write_file("contracts/requirements.txt", content)` - WRONG! No dependency lists in contracts/
|
|
118
|
+
❌ `write_file("contracts/config.yaml", content)` - WRONG! No config templates in contracts/
|
|
119
|
+
</BAD_EXAMPLES>
|
|
120
|
+
|
|
121
|
+
**Path format rule:** Always use `contracts/filename.ext`, never `.shotgun/contracts/filename.ext`
|
|
122
|
+
|
|
123
|
+
**Language-specific examples:**
|
|
124
|
+
|
|
125
|
+
<PYTHON_EXAMPLE>
|
|
126
|
+
# Python Pydantic model contract
|
|
127
|
+
from pydantic import BaseModel, Field
|
|
128
|
+
from typing import Optional
|
|
129
|
+
|
|
130
|
+
class User(BaseModel):
|
|
131
|
+
"""User model contract."""
|
|
132
|
+
id: int
|
|
133
|
+
email: str = Field(..., description="User email address")
|
|
134
|
+
username: str
|
|
135
|
+
is_active: bool = True
|
|
136
|
+
role: Optional[str] = None
|
|
137
|
+
|
|
138
|
+
# Save as: write_file("contracts/user_models.py", content)
|
|
139
|
+
</PYTHON_EXAMPLE>
|
|
140
|
+
|
|
141
|
+
<TYPESCRIPT_EXAMPLE>
|
|
142
|
+
// TypeScript interface contract
|
|
143
|
+
interface User {
|
|
144
|
+
id: number;
|
|
145
|
+
email: string;
|
|
146
|
+
username: string;
|
|
147
|
+
isActive: boolean;
|
|
148
|
+
role?: string;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
interface AuthToken {
|
|
152
|
+
token: string;
|
|
153
|
+
expiresAt: Date;
|
|
154
|
+
userId: number;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Save as: write_file("contracts/auth_types.ts", content)
|
|
158
|
+
</TYPESCRIPT_EXAMPLE>
|
|
159
|
+
|
|
160
|
+
<RUST_EXAMPLE>
|
|
161
|
+
// Rust struct contract
|
|
162
|
+
use serde::{Deserialize, Serialize};
|
|
163
|
+
|
|
164
|
+
#[derive(Debug, Serialize, Deserialize)]
|
|
165
|
+
pub struct User {
|
|
166
|
+
pub id: u64,
|
|
167
|
+
pub email: String,
|
|
168
|
+
pub username: String,
|
|
169
|
+
pub is_active: bool,
|
|
170
|
+
pub role: Option<String>,
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Save as: write_file("contracts/user_types.rs", content)
|
|
174
|
+
</RUST_EXAMPLE>
|
|
175
|
+
|
|
176
|
+
<OPENAPI_EXAMPLE>
|
|
177
|
+
{
|
|
178
|
+
"openapi": "3.0.0",
|
|
179
|
+
"info": {
|
|
180
|
+
"title": "User API",
|
|
181
|
+
"version": "1.0.0"
|
|
182
|
+
},
|
|
183
|
+
"paths": {
|
|
184
|
+
"/users": {
|
|
185
|
+
"get": {
|
|
186
|
+
"summary": "List users",
|
|
187
|
+
"responses": {
|
|
188
|
+
"200": {
|
|
189
|
+
"description": "Successful response",
|
|
190
|
+
"content": {
|
|
191
|
+
"application/json": {
|
|
192
|
+
"schema": {
|
|
193
|
+
"type": "array",
|
|
194
|
+
"items": { "$ref": "#/components/schemas/User" }
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
},
|
|
203
|
+
"components": {
|
|
204
|
+
"schemas": {
|
|
205
|
+
"User": {
|
|
206
|
+
"type": "object",
|
|
207
|
+
"properties": {
|
|
208
|
+
"id": { "type": "integer" },
|
|
209
|
+
"email": { "type": "string" },
|
|
210
|
+
"username": { "type": "string" }
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Save as: write_file("contracts/user_api.json", content)
|
|
218
|
+
</OPENAPI_EXAMPLE>
|
|
219
|
+
|
|
220
|
+
## WHAT IS ALLOWED vs WHAT IS FORBIDDEN
|
|
221
|
+
|
|
222
|
+
**✅ ALLOWED - Type Definitions (Shape and Structure):**
|
|
223
|
+
|
|
224
|
+
```python
|
|
225
|
+
# ✅ GOOD: Pydantic model (type definition)
|
|
226
|
+
from pydantic import BaseModel
|
|
227
|
+
|
|
228
|
+
class User(BaseModel):
|
|
229
|
+
id: int
|
|
230
|
+
email: str
|
|
231
|
+
username: str
|
|
232
|
+
|
|
233
|
+
# ✅ GOOD: Protocol class (interface definition)
|
|
234
|
+
from typing import Protocol
|
|
235
|
+
|
|
236
|
+
class Storage(Protocol):
|
|
237
|
+
def save(self, data: str) -> None: ...
|
|
238
|
+
def load(self) -> str: ...
|
|
239
|
+
|
|
240
|
+
# ✅ GOOD: Type aliases and enums
|
|
241
|
+
from typing import Literal
|
|
242
|
+
from enum import Enum
|
|
243
|
+
|
|
244
|
+
UserRole = Literal["admin", "user", "guest"]
|
|
245
|
+
|
|
246
|
+
class Status(Enum):
|
|
247
|
+
ACTIVE = "active"
|
|
248
|
+
INACTIVE = "inactive"
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
**❌ FORBIDDEN - Executable Code (Behavior and Logic):**
|
|
252
|
+
|
|
253
|
+
```python
|
|
254
|
+
# ❌ BAD: Function with pass (executable code)
|
|
255
|
+
def main() -> int:
|
|
256
|
+
pass
|
|
257
|
+
|
|
258
|
+
# ❌ BAD: Function with implementation
|
|
259
|
+
def validate_input(x: str) -> str:
|
|
260
|
+
return x.strip()
|
|
261
|
+
|
|
262
|
+
# ❌ BAD: Class with method implementations
|
|
263
|
+
class HistoryManager:
|
|
264
|
+
def __init__(self):
|
|
265
|
+
pass
|
|
266
|
+
|
|
267
|
+
def add_message(self, msg: str):
|
|
268
|
+
pass
|
|
269
|
+
|
|
270
|
+
# ❌ BAD: Data constants (runtime values)
|
|
271
|
+
SUPPORTED_PROVIDERS = [
|
|
272
|
+
{"name": "openai", "key": "OPENAI_API_KEY"}
|
|
273
|
+
]
|
|
274
|
+
|
|
275
|
+
# ❌ BAD: Helper functions
|
|
276
|
+
def get_default_config() -> dict:
|
|
277
|
+
return {"model": "gpt-4"}
|
|
278
|
+
|
|
279
|
+
# ❌ BAD: Executable code blocks
|
|
280
|
+
if __name__ == "__main__":
|
|
281
|
+
main()
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
**Remember**: Contracts define **SHAPES** (types, interfaces, schemas), NOT **BEHAVIOR** (functions, logic, implementations).
|
|
285
|
+
|
|
19
286
|
## AI AGENT PIPELINE AWARENESS
|
|
20
287
|
|
|
21
288
|
**CRITICAL**: Your output will be consumed by AI coding agents (Claude Code, Cursor, Windsurf, etc.)
|
shotgun/prompts/agents/tasks.j2
CHANGED
|
@@ -99,7 +99,7 @@ Then organize tasks into logical sections:
|
|
|
99
99
|
USER INTERACTION - ASK CLARIFYING QUESTIONS:
|
|
100
100
|
|
|
101
101
|
- ALWAYS ask clarifying questions when the request is vague or ambiguous
|
|
102
|
-
- Use
|
|
102
|
+
- Use clarifying questions to gather specific details about:
|
|
103
103
|
- Specific features or functionality to prioritize
|
|
104
104
|
- Technical constraints or preferences
|
|
105
105
|
- Timeline and resource constraints
|
shotgun/sentry_telemetry.py
CHANGED
|
@@ -1,14 +1,132 @@
|
|
|
1
1
|
"""Sentry observability setup for Shotgun."""
|
|
2
2
|
|
|
3
|
-
import
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Any
|
|
4
5
|
|
|
5
6
|
from shotgun import __version__
|
|
6
7
|
from shotgun.logging_config import get_early_logger
|
|
8
|
+
from shotgun.settings import settings
|
|
7
9
|
|
|
8
10
|
# Use early logger to prevent automatic StreamHandler creation
|
|
9
11
|
logger = get_early_logger(__name__)
|
|
10
12
|
|
|
11
13
|
|
|
14
|
+
def _scrub_path(path: str) -> str:
|
|
15
|
+
"""Scrub sensitive information from file paths.
|
|
16
|
+
|
|
17
|
+
Removes home directory and current working directory prefixes to prevent
|
|
18
|
+
leaking usernames that might be part of the path.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
path: The file path to scrub
|
|
22
|
+
|
|
23
|
+
Returns:
|
|
24
|
+
The scrubbed path with sensitive prefixes removed
|
|
25
|
+
"""
|
|
26
|
+
if not path:
|
|
27
|
+
return path
|
|
28
|
+
|
|
29
|
+
try:
|
|
30
|
+
# Get home and cwd as Path objects for comparison
|
|
31
|
+
home = Path.home()
|
|
32
|
+
cwd = Path.cwd()
|
|
33
|
+
|
|
34
|
+
# Convert path to Path object
|
|
35
|
+
path_obj = Path(path)
|
|
36
|
+
|
|
37
|
+
# Try to make path relative to cwd first (most common case)
|
|
38
|
+
try:
|
|
39
|
+
relative_to_cwd = path_obj.relative_to(cwd)
|
|
40
|
+
return str(relative_to_cwd)
|
|
41
|
+
except ValueError:
|
|
42
|
+
pass
|
|
43
|
+
|
|
44
|
+
# Try to replace home directory with ~
|
|
45
|
+
try:
|
|
46
|
+
relative_to_home = path_obj.relative_to(home)
|
|
47
|
+
return f"~/{relative_to_home}"
|
|
48
|
+
except ValueError:
|
|
49
|
+
pass
|
|
50
|
+
|
|
51
|
+
# If path is absolute but not under cwd or home, just return filename
|
|
52
|
+
if path_obj.is_absolute():
|
|
53
|
+
return path_obj.name
|
|
54
|
+
|
|
55
|
+
# Return as-is if already relative
|
|
56
|
+
return path
|
|
57
|
+
|
|
58
|
+
except Exception:
|
|
59
|
+
# If anything goes wrong, return the original path
|
|
60
|
+
# Better to leak a path than break error reporting
|
|
61
|
+
return path
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _scrub_sensitive_paths(event: dict[str, Any]) -> None:
|
|
65
|
+
"""Scrub sensitive paths from Sentry event data.
|
|
66
|
+
|
|
67
|
+
Modifies the event in-place to remove:
|
|
68
|
+
- Home directory paths (might contain usernames)
|
|
69
|
+
- Current working directory paths (might contain usernames)
|
|
70
|
+
- Server name/hostname
|
|
71
|
+
- Paths in sys.argv
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
event: The Sentry event dictionary to scrub
|
|
75
|
+
"""
|
|
76
|
+
extra = event.get("extra", {})
|
|
77
|
+
if "sys.argv" in extra:
|
|
78
|
+
argv = extra["sys.argv"]
|
|
79
|
+
if isinstance(argv, list):
|
|
80
|
+
extra["sys.argv"] = [
|
|
81
|
+
_scrub_path(arg) if isinstance(arg, str) else arg for arg in argv
|
|
82
|
+
]
|
|
83
|
+
|
|
84
|
+
# Scrub server name if present
|
|
85
|
+
if "server_name" in event:
|
|
86
|
+
event["server_name"] = ""
|
|
87
|
+
|
|
88
|
+
# Scrub contexts that might contain paths
|
|
89
|
+
if "contexts" in event:
|
|
90
|
+
contexts = event["contexts"]
|
|
91
|
+
# Remove runtime context if it has CWD
|
|
92
|
+
if "runtime" in contexts:
|
|
93
|
+
if "cwd" in contexts["runtime"]:
|
|
94
|
+
del contexts["runtime"]["cwd"]
|
|
95
|
+
# Scrub sys.argv to remove paths
|
|
96
|
+
if "sys.argv" in contexts["runtime"]:
|
|
97
|
+
argv = contexts["runtime"]["sys.argv"]
|
|
98
|
+
if isinstance(argv, list):
|
|
99
|
+
contexts["runtime"]["sys.argv"] = [
|
|
100
|
+
_scrub_path(arg) if isinstance(arg, str) else arg
|
|
101
|
+
for arg in argv
|
|
102
|
+
]
|
|
103
|
+
|
|
104
|
+
# Scrub exception stack traces
|
|
105
|
+
if "exception" in event and "values" in event["exception"]:
|
|
106
|
+
for exception in event["exception"]["values"]:
|
|
107
|
+
if "stacktrace" in exception and "frames" in exception["stacktrace"]:
|
|
108
|
+
for frame in exception["stacktrace"]["frames"]:
|
|
109
|
+
# Scrub file paths
|
|
110
|
+
if "abs_path" in frame:
|
|
111
|
+
frame["abs_path"] = _scrub_path(frame["abs_path"])
|
|
112
|
+
if "filename" in frame:
|
|
113
|
+
frame["filename"] = _scrub_path(frame["filename"])
|
|
114
|
+
|
|
115
|
+
# Scrub local variables that might contain paths
|
|
116
|
+
if "vars" in frame:
|
|
117
|
+
for var_name, var_value in frame["vars"].items():
|
|
118
|
+
if isinstance(var_value, str):
|
|
119
|
+
frame["vars"][var_name] = _scrub_path(var_value)
|
|
120
|
+
|
|
121
|
+
# Scrub breadcrumbs that might contain paths
|
|
122
|
+
if "breadcrumbs" in event and "values" in event["breadcrumbs"]:
|
|
123
|
+
for breadcrumb in event["breadcrumbs"]["values"]:
|
|
124
|
+
if "data" in breadcrumb:
|
|
125
|
+
for key, value in breadcrumb["data"].items():
|
|
126
|
+
if isinstance(value, str):
|
|
127
|
+
breadcrumb["data"][key] = _scrub_path(value)
|
|
128
|
+
|
|
129
|
+
|
|
12
130
|
def setup_sentry_observability() -> bool:
|
|
13
131
|
"""Set up Sentry observability for error tracking.
|
|
14
132
|
|
|
@@ -23,48 +141,77 @@ def setup_sentry_observability() -> bool:
|
|
|
23
141
|
logger.debug("Sentry is already initialized, skipping")
|
|
24
142
|
return True
|
|
25
143
|
|
|
26
|
-
#
|
|
27
|
-
dsn =
|
|
28
|
-
try:
|
|
29
|
-
from shotgun import build_constants
|
|
30
|
-
|
|
31
|
-
dsn = build_constants.SENTRY_DSN
|
|
32
|
-
logger.debug("Using Sentry DSN from build constants")
|
|
33
|
-
except ImportError:
|
|
34
|
-
# Fallback to environment variable (development)
|
|
35
|
-
dsn = os.getenv("SENTRY_DSN", "")
|
|
36
|
-
if dsn:
|
|
37
|
-
logger.debug("Using Sentry DSN from environment variable")
|
|
144
|
+
# Get DSN from settings (handles build constants + env vars automatically)
|
|
145
|
+
dsn = settings.telemetry.sentry_dsn
|
|
38
146
|
|
|
39
147
|
if not dsn:
|
|
40
148
|
logger.debug("No Sentry DSN configured, skipping Sentry initialization")
|
|
41
149
|
return False
|
|
42
150
|
|
|
43
|
-
logger.debug("
|
|
151
|
+
logger.debug("Using Sentry DSN from settings, proceeding with setup")
|
|
44
152
|
|
|
45
153
|
# Determine environment based on version
|
|
46
|
-
# Dev versions contain "dev", "rc", "alpha",
|
|
154
|
+
# Dev versions contain "dev", "rc", "alpha", "beta"
|
|
47
155
|
if any(marker in __version__ for marker in ["dev", "rc", "alpha", "beta"]):
|
|
48
156
|
environment = "development"
|
|
49
157
|
else:
|
|
50
158
|
environment = "production"
|
|
51
159
|
|
|
160
|
+
def before_send(event: Any, hint: dict[str, Any]) -> Any:
|
|
161
|
+
"""Filter out user-actionable errors and scrub sensitive paths.
|
|
162
|
+
|
|
163
|
+
User-actionable errors (like context size limits) are expected conditions
|
|
164
|
+
that users need to resolve, not bugs that need tracking.
|
|
165
|
+
|
|
166
|
+
Also scrubs sensitive information like usernames from file paths and
|
|
167
|
+
working directories to protect user privacy.
|
|
168
|
+
"""
|
|
169
|
+
|
|
170
|
+
log_record = hint.get("log_record")
|
|
171
|
+
if log_record:
|
|
172
|
+
# Scrub pathname using the helper function
|
|
173
|
+
log_record.pathname = _scrub_path(log_record.pathname)
|
|
174
|
+
|
|
175
|
+
# Scrub traceback text if it exists
|
|
176
|
+
if hasattr(log_record, "exc_text") and isinstance(
|
|
177
|
+
log_record.exc_text, str
|
|
178
|
+
):
|
|
179
|
+
# Replace home directory in traceback text
|
|
180
|
+
home = Path.home()
|
|
181
|
+
log_record.exc_text = log_record.exc_text.replace(str(home), "~")
|
|
182
|
+
|
|
183
|
+
if "exc_info" in hint:
|
|
184
|
+
_, exc_value, _ = hint["exc_info"]
|
|
185
|
+
from shotgun.exceptions import ErrorNotPickedUpBySentry
|
|
186
|
+
|
|
187
|
+
if isinstance(exc_value, ErrorNotPickedUpBySentry):
|
|
188
|
+
# Don't send to Sentry - this is user-actionable, not a bug
|
|
189
|
+
return None
|
|
190
|
+
|
|
191
|
+
# Scrub sensitive paths from the event
|
|
192
|
+
_scrub_sensitive_paths(event)
|
|
193
|
+
return event
|
|
194
|
+
|
|
52
195
|
# Initialize Sentry
|
|
53
196
|
sentry_sdk.init(
|
|
54
197
|
dsn=dsn,
|
|
55
198
|
release=f"shotgun-sh@{__version__}",
|
|
56
199
|
environment=environment,
|
|
57
200
|
send_default_pii=False, # Privacy-first: never send PII
|
|
201
|
+
server_name="", # Privacy: don't send hostname (may contain username)
|
|
58
202
|
traces_sample_rate=0.1 if environment == "production" else 1.0,
|
|
59
203
|
profiles_sample_rate=0.1 if environment == "production" else 1.0,
|
|
204
|
+
before_send=before_send,
|
|
60
205
|
)
|
|
61
206
|
|
|
62
207
|
# Set user context with anonymous shotgun instance ID from config
|
|
63
208
|
try:
|
|
209
|
+
import asyncio
|
|
210
|
+
|
|
64
211
|
from shotgun.agents.config import get_config_manager
|
|
65
212
|
|
|
66
213
|
config_manager = get_config_manager()
|
|
67
|
-
shotgun_instance_id = config_manager.get_shotgun_instance_id()
|
|
214
|
+
shotgun_instance_id = asyncio.run(config_manager.get_shotgun_instance_id())
|
|
68
215
|
sentry_sdk.set_user({"id": shotgun_instance_id})
|
|
69
216
|
logger.debug("Sentry user context set with anonymous ID")
|
|
70
217
|
except Exception as e:
|