shotgun-sh 0.1.0.dev6__py3-none-any.whl → 0.1.0.dev8__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.
Potentially problematic release.
This version of shotgun-sh might be problematic. Click here for more details.
- shotgun/__init__.py +3 -1
- shotgun/agents/config/manager.py +34 -7
- shotgun/agents/config/models.py +2 -0
- shotgun/build_constants.py +20 -0
- shotgun/cli/config.py +14 -0
- shotgun/cli/plan.py +11 -0
- shotgun/cli/research.py +11 -0
- shotgun/cli/tasks.py +11 -0
- shotgun/cli/update.py +152 -0
- shotgun/main.py +73 -3
- shotgun/posthog_telemetry.py +156 -0
- shotgun/sentry_telemetry.py +85 -0
- shotgun/telemetry.py +45 -3
- shotgun/tui/app.py +38 -3
- shotgun/utils/update_checker.py +375 -0
- {shotgun_sh-0.1.0.dev6.dist-info → shotgun_sh-0.1.0.dev8.dist-info}/METADATA +150 -1
- {shotgun_sh-0.1.0.dev6.dist-info → shotgun_sh-0.1.0.dev8.dist-info}/RECORD +20 -15
- {shotgun_sh-0.1.0.dev6.dist-info → shotgun_sh-0.1.0.dev8.dist-info}/WHEEL +0 -0
- {shotgun_sh-0.1.0.dev6.dist-info → shotgun_sh-0.1.0.dev8.dist-info}/entry_points.txt +0 -0
- {shotgun_sh-0.1.0.dev6.dist-info → shotgun_sh-0.1.0.dev8.dist-info}/licenses/LICENSE +0 -0
shotgun/__init__.py
CHANGED
shotgun/agents/config/manager.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""Configuration manager for Shotgun CLI."""
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
|
+
import uuid
|
|
4
5
|
from pathlib import Path
|
|
5
6
|
from typing import Any
|
|
6
7
|
|
|
@@ -41,9 +42,11 @@ class ConfigManager:
|
|
|
41
42
|
|
|
42
43
|
if not self.config_path.exists():
|
|
43
44
|
logger.info(
|
|
44
|
-
"Configuration file not found,
|
|
45
|
+
"Configuration file not found, creating new config with user_id: %s",
|
|
46
|
+
self.config_path,
|
|
45
47
|
)
|
|
46
|
-
|
|
48
|
+
# Create new config with generated user_id
|
|
49
|
+
self._config = self.initialize()
|
|
47
50
|
return self._config
|
|
48
51
|
|
|
49
52
|
try:
|
|
@@ -61,8 +64,8 @@ class ConfigManager:
|
|
|
61
64
|
logger.error(
|
|
62
65
|
"Failed to load configuration from %s: %s", self.config_path, e
|
|
63
66
|
)
|
|
64
|
-
logger.info("
|
|
65
|
-
self._config =
|
|
67
|
+
logger.info("Creating new configuration with generated user_id")
|
|
68
|
+
self._config = self.initialize()
|
|
66
69
|
return self._config
|
|
67
70
|
|
|
68
71
|
def save(self, config: ShotgunConfig | None = None) -> None:
|
|
@@ -72,7 +75,14 @@ class ConfigManager:
|
|
|
72
75
|
config: Configuration to save. If None, saves current loaded config
|
|
73
76
|
"""
|
|
74
77
|
if config is None:
|
|
75
|
-
|
|
78
|
+
if self._config:
|
|
79
|
+
config = self._config
|
|
80
|
+
else:
|
|
81
|
+
# Create a new config with generated user_id
|
|
82
|
+
config = ShotgunConfig(
|
|
83
|
+
user_id=str(uuid.uuid4()),
|
|
84
|
+
config_version=1,
|
|
85
|
+
)
|
|
76
86
|
|
|
77
87
|
# Ensure directory exists
|
|
78
88
|
self.config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
@@ -150,9 +160,17 @@ class ConfigManager:
|
|
|
150
160
|
Returns:
|
|
151
161
|
Default ShotgunConfig
|
|
152
162
|
"""
|
|
153
|
-
|
|
163
|
+
# Generate unique user ID for new config
|
|
164
|
+
config = ShotgunConfig(
|
|
165
|
+
user_id=str(uuid.uuid4()),
|
|
166
|
+
config_version=1,
|
|
167
|
+
)
|
|
154
168
|
self.save(config)
|
|
155
|
-
logger.info(
|
|
169
|
+
logger.info(
|
|
170
|
+
"Configuration initialized at %s with user_id: %s",
|
|
171
|
+
self.config_path,
|
|
172
|
+
config.user_id,
|
|
173
|
+
)
|
|
156
174
|
return config
|
|
157
175
|
|
|
158
176
|
def _convert_secrets_to_secretstr(self, data: dict[str, Any]) -> None:
|
|
@@ -209,6 +227,15 @@ class ConfigManager:
|
|
|
209
227
|
|
|
210
228
|
return bool(value.strip())
|
|
211
229
|
|
|
230
|
+
def get_user_id(self) -> str:
|
|
231
|
+
"""Get the user ID from configuration.
|
|
232
|
+
|
|
233
|
+
Returns:
|
|
234
|
+
The unique user ID string
|
|
235
|
+
"""
|
|
236
|
+
config = self.load()
|
|
237
|
+
return config.user_id
|
|
238
|
+
|
|
212
239
|
|
|
213
240
|
def get_config_manager() -> ConfigManager:
|
|
214
241
|
"""Get the global ConfigManager instance."""
|
shotgun/agents/config/models.py
CHANGED
|
@@ -123,3 +123,5 @@ class ShotgunConfig(BaseModel):
|
|
|
123
123
|
default_provider: ProviderType = Field(
|
|
124
124
|
default=ProviderType.OPENAI, description="Default AI provider to use"
|
|
125
125
|
)
|
|
126
|
+
user_id: str = Field(description="Unique anonymous user identifier")
|
|
127
|
+
config_version: int = Field(default=1, description="Configuration schema version")
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""Build-time constants generated during packaging.
|
|
2
|
+
|
|
3
|
+
This file is auto-generated during the build process.
|
|
4
|
+
DO NOT EDIT MANUALLY.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
# Sentry DSN embedded at build time (empty string if not provided)
|
|
8
|
+
SENTRY_DSN = 'https://2818a6d165c64eccc94cfd51ce05d6aa@o4506813296738304.ingest.us.sentry.io/4510045952409600'
|
|
9
|
+
|
|
10
|
+
# PostHog configuration embedded at build time (empty strings if not provided)
|
|
11
|
+
POSTHOG_API_KEY = ''
|
|
12
|
+
POSTHOG_PROJECT_ID = '191396'
|
|
13
|
+
|
|
14
|
+
# Logfire configuration embedded at build time (only for dev builds)
|
|
15
|
+
LOGFIRE_ENABLED = 'true'
|
|
16
|
+
LOGFIRE_TOKEN = 'pylf_v1_us_KZ5NM1pP3NwgJkbBJt6Ftdzk8mMhmrXcGJHQQgDJ1LfK'
|
|
17
|
+
|
|
18
|
+
# Build metadata
|
|
19
|
+
BUILD_TIME_ENV = "production" if SENTRY_DSN else "development"
|
|
20
|
+
IS_DEV_BUILD = True
|
shotgun/cli/config.py
CHANGED
|
@@ -259,3 +259,17 @@ def _mask_value(value: str) -> str:
|
|
|
259
259
|
if len(value) <= 8:
|
|
260
260
|
return "••••••••"
|
|
261
261
|
return f"{value[:4]}{'•' * (len(value) - 8)}{value[-4:]}"
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
@app.command()
|
|
265
|
+
def get_user_id() -> None:
|
|
266
|
+
"""Get the anonymous user ID from configuration."""
|
|
267
|
+
config_manager = get_config_manager()
|
|
268
|
+
|
|
269
|
+
try:
|
|
270
|
+
user_id = config_manager.get_user_id()
|
|
271
|
+
console.print(f"[green]User ID:[/green] {user_id}")
|
|
272
|
+
except Exception as e:
|
|
273
|
+
logger.error(f"Error getting user ID: {e}")
|
|
274
|
+
console.print(f"❌ Failed to get user ID: {str(e)}", style="red")
|
|
275
|
+
raise typer.Exit(1) from e
|
shotgun/cli/plan.py
CHANGED
|
@@ -38,6 +38,17 @@ def plan(
|
|
|
38
38
|
logger.info("📋 Planning Goal: %s", goal)
|
|
39
39
|
|
|
40
40
|
try:
|
|
41
|
+
# Track plan command usage
|
|
42
|
+
from shotgun.posthog_telemetry import track_event
|
|
43
|
+
|
|
44
|
+
track_event(
|
|
45
|
+
"plan_command",
|
|
46
|
+
{
|
|
47
|
+
"non_interactive": non_interactive,
|
|
48
|
+
"provider": provider.value if provider else "default",
|
|
49
|
+
},
|
|
50
|
+
)
|
|
51
|
+
|
|
41
52
|
# Create agent dependencies
|
|
42
53
|
agent_runtime_options = AgentRuntimeOptions(
|
|
43
54
|
interactive_mode=not non_interactive
|
shotgun/cli/research.py
CHANGED
|
@@ -58,6 +58,17 @@ async def async_research(
|
|
|
58
58
|
provider: ProviderType | None = None,
|
|
59
59
|
) -> None:
|
|
60
60
|
"""Async wrapper for research process."""
|
|
61
|
+
# Track research command usage
|
|
62
|
+
from shotgun.posthog_telemetry import track_event
|
|
63
|
+
|
|
64
|
+
track_event(
|
|
65
|
+
"research_command",
|
|
66
|
+
{
|
|
67
|
+
"non_interactive": non_interactive,
|
|
68
|
+
"provider": provider.value if provider else "default",
|
|
69
|
+
},
|
|
70
|
+
)
|
|
71
|
+
|
|
61
72
|
# Create agent dependencies
|
|
62
73
|
agent_runtime_options = AgentRuntimeOptions(interactive_mode=not non_interactive)
|
|
63
74
|
|
shotgun/cli/tasks.py
CHANGED
|
@@ -44,6 +44,17 @@ def tasks(
|
|
|
44
44
|
logger.info("📋 Task Creation Instruction: %s", instruction)
|
|
45
45
|
|
|
46
46
|
try:
|
|
47
|
+
# Track tasks command usage
|
|
48
|
+
from shotgun.posthog_telemetry import track_event
|
|
49
|
+
|
|
50
|
+
track_event(
|
|
51
|
+
"tasks_command",
|
|
52
|
+
{
|
|
53
|
+
"non_interactive": non_interactive,
|
|
54
|
+
"provider": provider.value if provider else "default",
|
|
55
|
+
},
|
|
56
|
+
)
|
|
57
|
+
|
|
47
58
|
# Create agent dependencies
|
|
48
59
|
agent_runtime_options = AgentRuntimeOptions(
|
|
49
60
|
interactive_mode=not non_interactive
|
shotgun/cli/update.py
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"""Update command for shotgun CLI."""
|
|
2
|
+
|
|
3
|
+
from typing import Annotated
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
from rich.console import Console
|
|
7
|
+
from rich.progress import Progress, SpinnerColumn, TextColumn
|
|
8
|
+
|
|
9
|
+
from shotgun.logging_config import get_logger
|
|
10
|
+
from shotgun.utils.update_checker import (
|
|
11
|
+
detect_installation_method,
|
|
12
|
+
get_latest_version,
|
|
13
|
+
is_dev_version,
|
|
14
|
+
perform_update,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
logger = get_logger(__name__)
|
|
18
|
+
console = Console()
|
|
19
|
+
app = typer.Typer()
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@app.callback(invoke_without_command=True)
|
|
23
|
+
def update(
|
|
24
|
+
ctx: typer.Context,
|
|
25
|
+
force: Annotated[
|
|
26
|
+
bool,
|
|
27
|
+
typer.Option(
|
|
28
|
+
"--force",
|
|
29
|
+
"-f",
|
|
30
|
+
help="Force update even for development versions",
|
|
31
|
+
),
|
|
32
|
+
] = False,
|
|
33
|
+
check_only: Annotated[
|
|
34
|
+
bool,
|
|
35
|
+
typer.Option(
|
|
36
|
+
"--check",
|
|
37
|
+
"-c",
|
|
38
|
+
help="Check for updates without installing",
|
|
39
|
+
),
|
|
40
|
+
] = False,
|
|
41
|
+
) -> None:
|
|
42
|
+
"""Check for and install updates to shotgun-sh.
|
|
43
|
+
|
|
44
|
+
This command will:
|
|
45
|
+
- Check PyPI for the latest version
|
|
46
|
+
- Detect your installation method (pipx, pip, or venv)
|
|
47
|
+
- Perform the appropriate upgrade command
|
|
48
|
+
|
|
49
|
+
Examples:
|
|
50
|
+
shotgun update # Check and install updates
|
|
51
|
+
shotgun update --check # Only check for updates
|
|
52
|
+
shotgun update --force # Force update (even for dev versions)
|
|
53
|
+
"""
|
|
54
|
+
if ctx.resilient_parsing:
|
|
55
|
+
return
|
|
56
|
+
|
|
57
|
+
# Handle check-only mode
|
|
58
|
+
if check_only:
|
|
59
|
+
with Progress(
|
|
60
|
+
SpinnerColumn(),
|
|
61
|
+
TextColumn("[progress.description]{task.description}"),
|
|
62
|
+
console=console,
|
|
63
|
+
) as progress:
|
|
64
|
+
progress.add_task("Checking for updates...", total=None)
|
|
65
|
+
|
|
66
|
+
latest = get_latest_version()
|
|
67
|
+
if not latest:
|
|
68
|
+
console.print(
|
|
69
|
+
"[red]✗[/red] Failed to check for updates", style="bold red"
|
|
70
|
+
)
|
|
71
|
+
raise typer.Exit(1)
|
|
72
|
+
|
|
73
|
+
from shotgun import __version__
|
|
74
|
+
from shotgun.utils.update_checker import compare_versions
|
|
75
|
+
|
|
76
|
+
if compare_versions(__version__, latest):
|
|
77
|
+
console.print(
|
|
78
|
+
f"[green]✓[/green] Update available: [cyan]{__version__}[/cyan] → [green]{latest}[/green]",
|
|
79
|
+
style="bold",
|
|
80
|
+
)
|
|
81
|
+
console.print("Run 'shotgun update' to install the update")
|
|
82
|
+
else:
|
|
83
|
+
console.print(
|
|
84
|
+
f"[green]✓[/green] You're on the latest version ([cyan]{__version__}[/cyan])",
|
|
85
|
+
style="bold",
|
|
86
|
+
)
|
|
87
|
+
return
|
|
88
|
+
|
|
89
|
+
# Check for dev version
|
|
90
|
+
if is_dev_version() and not force:
|
|
91
|
+
console.print(
|
|
92
|
+
"[yellow]⚠[/yellow] You're running a development version",
|
|
93
|
+
style="bold yellow",
|
|
94
|
+
)
|
|
95
|
+
console.print(
|
|
96
|
+
"Use --force to update anyway, or install the stable version with:\n"
|
|
97
|
+
" pipx install shotgun-sh\n"
|
|
98
|
+
" or\n"
|
|
99
|
+
" pip install shotgun-sh",
|
|
100
|
+
)
|
|
101
|
+
raise typer.Exit(1)
|
|
102
|
+
|
|
103
|
+
# Confirm if forcing dev version update
|
|
104
|
+
if is_dev_version() and force:
|
|
105
|
+
confirm = typer.confirm(
|
|
106
|
+
"⚠️ You're about to replace a development version. Continue?",
|
|
107
|
+
default=False,
|
|
108
|
+
)
|
|
109
|
+
if not confirm:
|
|
110
|
+
console.print("Update cancelled", style="dim")
|
|
111
|
+
raise typer.Exit(0)
|
|
112
|
+
|
|
113
|
+
# Detect installation method
|
|
114
|
+
method = detect_installation_method()
|
|
115
|
+
console.print(f"Installation method: [cyan]{method}[/cyan]", style="dim")
|
|
116
|
+
|
|
117
|
+
# Perform update
|
|
118
|
+
with Progress(
|
|
119
|
+
SpinnerColumn(),
|
|
120
|
+
TextColumn("[progress.description]{task.description}"),
|
|
121
|
+
console=console,
|
|
122
|
+
) as progress:
|
|
123
|
+
task = progress.add_task("Updating shotgun-sh...", total=None)
|
|
124
|
+
|
|
125
|
+
success, message = perform_update(force=force)
|
|
126
|
+
|
|
127
|
+
if success:
|
|
128
|
+
progress.update(task, description="[green]✓[/green] Update complete!")
|
|
129
|
+
console.print(f"\n[green]✓[/green] {message}", style="bold green")
|
|
130
|
+
console.print(
|
|
131
|
+
"\n[dim]Restart your terminal or run 'shotgun --version' to verify the update[/dim]"
|
|
132
|
+
)
|
|
133
|
+
else:
|
|
134
|
+
progress.update(task, description="[red]✗[/red] Update failed")
|
|
135
|
+
console.print(f"\n[red]✗[/red] {message}", style="bold red")
|
|
136
|
+
|
|
137
|
+
# Provide manual update instructions
|
|
138
|
+
if method == "pipx":
|
|
139
|
+
console.print(
|
|
140
|
+
"\n[yellow]Try updating manually:[/yellow]\n"
|
|
141
|
+
" pipx upgrade shotgun-sh"
|
|
142
|
+
)
|
|
143
|
+
else:
|
|
144
|
+
console.print(
|
|
145
|
+
"\n[yellow]Try updating manually:[/yellow]\n"
|
|
146
|
+
" pip install --upgrade shotgun-sh"
|
|
147
|
+
)
|
|
148
|
+
raise typer.Exit(1)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
if __name__ == "__main__":
|
|
152
|
+
app()
|
shotgun/main.py
CHANGED
|
@@ -5,11 +5,15 @@ from typing import Annotated
|
|
|
5
5
|
import typer
|
|
6
6
|
from dotenv import load_dotenv
|
|
7
7
|
|
|
8
|
+
from shotgun import __version__
|
|
8
9
|
from shotgun.agents.config import get_config_manager
|
|
9
|
-
from shotgun.cli import codebase, config, plan, research, tasks
|
|
10
|
+
from shotgun.cli import codebase, config, plan, research, tasks, update
|
|
10
11
|
from shotgun.logging_config import configure_root_logger, get_logger
|
|
12
|
+
from shotgun.posthog_telemetry import setup_posthog_observability
|
|
13
|
+
from shotgun.sentry_telemetry import setup_sentry_observability
|
|
11
14
|
from shotgun.telemetry import setup_logfire_observability
|
|
12
15
|
from shotgun.tui import app as tui_app
|
|
16
|
+
from shotgun.utils.update_checker import check_for_updates_async
|
|
13
17
|
|
|
14
18
|
# Load environment variables from .env file
|
|
15
19
|
load_dotenv()
|
|
@@ -29,6 +33,24 @@ except Exception as e:
|
|
|
29
33
|
_logfire_enabled = setup_logfire_observability()
|
|
30
34
|
logger.debug("Logfire observability enabled: %s", _logfire_enabled)
|
|
31
35
|
|
|
36
|
+
# Initialize Sentry telemetry
|
|
37
|
+
_sentry_enabled = setup_sentry_observability()
|
|
38
|
+
logger.debug("Sentry observability enabled: %s", _sentry_enabled)
|
|
39
|
+
|
|
40
|
+
# Initialize PostHog analytics
|
|
41
|
+
_posthog_enabled = setup_posthog_observability()
|
|
42
|
+
logger.debug("PostHog analytics enabled: %s", _posthog_enabled)
|
|
43
|
+
|
|
44
|
+
# Global variable to store update notification
|
|
45
|
+
_update_notification: str | None = None
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _update_callback(notification: str) -> None:
|
|
49
|
+
"""Callback to store update notification."""
|
|
50
|
+
global _update_notification
|
|
51
|
+
_update_notification = notification
|
|
52
|
+
|
|
53
|
+
|
|
32
54
|
app = typer.Typer(
|
|
33
55
|
name="shotgun",
|
|
34
56
|
help="Shotgun - AI-powered CLI tool for research, planning, and task management",
|
|
@@ -43,12 +65,16 @@ app.add_typer(
|
|
|
43
65
|
app.add_typer(research.app, name="research", help="Perform research with agentic loops")
|
|
44
66
|
app.add_typer(plan.app, name="plan", help="Generate structured plans")
|
|
45
67
|
app.add_typer(tasks.app, name="tasks", help="Generate task lists with agentic approach")
|
|
68
|
+
app.add_typer(update.app, name="update", help="Check for and install updates")
|
|
46
69
|
|
|
47
70
|
|
|
48
71
|
def version_callback(value: bool) -> None:
|
|
49
72
|
"""Show version and exit."""
|
|
50
73
|
if value:
|
|
51
|
-
|
|
74
|
+
from rich.console import Console
|
|
75
|
+
|
|
76
|
+
console = Console()
|
|
77
|
+
console.print(f"shotgun {__version__}")
|
|
52
78
|
raise typer.Exit()
|
|
53
79
|
|
|
54
80
|
|
|
@@ -65,14 +91,58 @@ def main(
|
|
|
65
91
|
help="Show version and exit",
|
|
66
92
|
),
|
|
67
93
|
] = False,
|
|
94
|
+
no_update_check: Annotated[
|
|
95
|
+
bool,
|
|
96
|
+
typer.Option(
|
|
97
|
+
"--no-update-check",
|
|
98
|
+
help="Disable automatic update checks",
|
|
99
|
+
),
|
|
100
|
+
] = False,
|
|
68
101
|
) -> None:
|
|
69
102
|
"""Shotgun - AI-powered CLI tool."""
|
|
70
103
|
logger.debug("Starting shotgun CLI application")
|
|
104
|
+
|
|
105
|
+
# Start async update check (non-blocking)
|
|
106
|
+
if not ctx.resilient_parsing:
|
|
107
|
+
check_for_updates_async(
|
|
108
|
+
callback=_update_callback, no_update_check=no_update_check
|
|
109
|
+
)
|
|
110
|
+
|
|
71
111
|
if ctx.invoked_subcommand is None and not ctx.resilient_parsing:
|
|
72
112
|
logger.debug("Launching shotgun TUI application")
|
|
73
|
-
tui_app.run()
|
|
113
|
+
tui_app.run(no_update_check=no_update_check)
|
|
114
|
+
|
|
115
|
+
# Show update notification after TUI exits
|
|
116
|
+
if _update_notification:
|
|
117
|
+
from rich.console import Console
|
|
118
|
+
|
|
119
|
+
console = Console()
|
|
120
|
+
console.print(f"\n[cyan]{_update_notification}[/cyan]", style="bold")
|
|
121
|
+
|
|
74
122
|
raise typer.Exit()
|
|
75
123
|
|
|
124
|
+
# For CLI commands, we'll show notification at the end
|
|
125
|
+
# This is handled by registering an atexit handler
|
|
126
|
+
if not ctx.resilient_parsing and ctx.invoked_subcommand is not None:
|
|
127
|
+
import atexit
|
|
128
|
+
|
|
129
|
+
def show_update_notification() -> None:
|
|
130
|
+
if _update_notification:
|
|
131
|
+
from rich.console import Console
|
|
132
|
+
|
|
133
|
+
console = Console()
|
|
134
|
+
console.print(f"\n[cyan]{_update_notification}[/cyan]", style="bold")
|
|
135
|
+
|
|
136
|
+
atexit.register(show_update_notification)
|
|
137
|
+
|
|
138
|
+
# Register PostHog shutdown handler
|
|
139
|
+
def shutdown_posthog() -> None:
|
|
140
|
+
from shotgun.posthog_telemetry import shutdown
|
|
141
|
+
|
|
142
|
+
shutdown()
|
|
143
|
+
|
|
144
|
+
atexit.register(shutdown_posthog)
|
|
145
|
+
|
|
76
146
|
|
|
77
147
|
if __name__ == "__main__":
|
|
78
148
|
app()
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"""PostHog analytics setup for Shotgun."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import os
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
logger = logging.getLogger(__name__)
|
|
8
|
+
|
|
9
|
+
# Global PostHog client instance
|
|
10
|
+
_posthog_client = None
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def setup_posthog_observability() -> bool:
|
|
14
|
+
"""Set up PostHog analytics for usage tracking.
|
|
15
|
+
|
|
16
|
+
Returns:
|
|
17
|
+
True if PostHog was successfully set up, False otherwise
|
|
18
|
+
"""
|
|
19
|
+
global _posthog_client
|
|
20
|
+
|
|
21
|
+
try:
|
|
22
|
+
import posthog
|
|
23
|
+
|
|
24
|
+
# Check if PostHog is already initialized
|
|
25
|
+
if _posthog_client is not None:
|
|
26
|
+
logger.debug("PostHog is already initialized, skipping")
|
|
27
|
+
return True
|
|
28
|
+
|
|
29
|
+
# Try to get API key from build constants first (production builds)
|
|
30
|
+
api_key = None
|
|
31
|
+
|
|
32
|
+
try:
|
|
33
|
+
from shotgun import build_constants
|
|
34
|
+
|
|
35
|
+
api_key = build_constants.POSTHOG_API_KEY
|
|
36
|
+
if api_key:
|
|
37
|
+
logger.debug("Using PostHog configuration from build constants")
|
|
38
|
+
except (ImportError, AttributeError):
|
|
39
|
+
pass
|
|
40
|
+
|
|
41
|
+
# Fallback to environment variables if build constants are empty or missing
|
|
42
|
+
if not api_key:
|
|
43
|
+
api_key = os.getenv("POSTHOG_API_KEY", "")
|
|
44
|
+
if api_key:
|
|
45
|
+
logger.debug("Using PostHog configuration from environment variables")
|
|
46
|
+
|
|
47
|
+
if not api_key:
|
|
48
|
+
logger.debug(
|
|
49
|
+
"No PostHog API key configured, skipping PostHog initialization"
|
|
50
|
+
)
|
|
51
|
+
return False
|
|
52
|
+
|
|
53
|
+
logger.debug("Found PostHog configuration, proceeding with setup")
|
|
54
|
+
|
|
55
|
+
# Get version for context
|
|
56
|
+
from shotgun import __version__
|
|
57
|
+
|
|
58
|
+
# Determine environment based on version
|
|
59
|
+
# Dev versions contain "dev", "rc", "alpha", or "beta"
|
|
60
|
+
if any(marker in __version__ for marker in ["dev", "rc", "alpha", "beta"]):
|
|
61
|
+
environment = "development"
|
|
62
|
+
else:
|
|
63
|
+
environment = "production"
|
|
64
|
+
|
|
65
|
+
# Initialize PostHog client
|
|
66
|
+
posthog.api_key = api_key
|
|
67
|
+
posthog.host = "https://us.i.posthog.com" # Use US cloud instance
|
|
68
|
+
|
|
69
|
+
# Store the client for later use
|
|
70
|
+
_posthog_client = posthog
|
|
71
|
+
|
|
72
|
+
# Set user context with anonymous user ID from config
|
|
73
|
+
try:
|
|
74
|
+
from shotgun.agents.config import get_config_manager
|
|
75
|
+
|
|
76
|
+
config_manager = get_config_manager()
|
|
77
|
+
user_id = config_manager.get_user_id()
|
|
78
|
+
|
|
79
|
+
# Set default properties for all events
|
|
80
|
+
posthog.disabled = False
|
|
81
|
+
posthog.personal_api_key = None # Not needed for event tracking
|
|
82
|
+
|
|
83
|
+
logger.debug(
|
|
84
|
+
"PostHog user context will be set with anonymous ID: %s", user_id
|
|
85
|
+
)
|
|
86
|
+
except Exception as e:
|
|
87
|
+
logger.warning("Failed to get user context: %s", e)
|
|
88
|
+
|
|
89
|
+
logger.debug(
|
|
90
|
+
"PostHog analytics configured successfully (environment: %s, version: %s)",
|
|
91
|
+
environment,
|
|
92
|
+
__version__,
|
|
93
|
+
)
|
|
94
|
+
return True
|
|
95
|
+
|
|
96
|
+
except ImportError as e:
|
|
97
|
+
logger.error("PostHog SDK not available: %s", e)
|
|
98
|
+
return False
|
|
99
|
+
except Exception as e:
|
|
100
|
+
logger.warning("Failed to setup PostHog analytics: %s", e)
|
|
101
|
+
return False
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def track_event(event_name: str, properties: dict[str, Any] | None = None) -> None:
|
|
105
|
+
"""Track an event in PostHog.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
event_name: Name of the event to track
|
|
109
|
+
properties: Optional properties to include with the event
|
|
110
|
+
"""
|
|
111
|
+
global _posthog_client
|
|
112
|
+
|
|
113
|
+
if _posthog_client is None:
|
|
114
|
+
logger.debug("PostHog not initialized, skipping event: %s", event_name)
|
|
115
|
+
return
|
|
116
|
+
|
|
117
|
+
try:
|
|
118
|
+
from shotgun import __version__
|
|
119
|
+
from shotgun.agents.config import get_config_manager
|
|
120
|
+
|
|
121
|
+
# Get user ID for tracking
|
|
122
|
+
config_manager = get_config_manager()
|
|
123
|
+
user_id = config_manager.get_user_id()
|
|
124
|
+
|
|
125
|
+
# Add version and environment to properties
|
|
126
|
+
if properties is None:
|
|
127
|
+
properties = {}
|
|
128
|
+
properties["version"] = __version__
|
|
129
|
+
|
|
130
|
+
# Determine environment
|
|
131
|
+
if any(marker in __version__ for marker in ["dev", "rc", "alpha", "beta"]):
|
|
132
|
+
properties["environment"] = "development"
|
|
133
|
+
else:
|
|
134
|
+
properties["environment"] = "production"
|
|
135
|
+
|
|
136
|
+
# Track the event using PostHog's capture method
|
|
137
|
+
_posthog_client.capture(
|
|
138
|
+
distinct_id=user_id, event=event_name, properties=properties
|
|
139
|
+
)
|
|
140
|
+
logger.debug("Tracked PostHog event: %s", event_name)
|
|
141
|
+
except Exception as e:
|
|
142
|
+
logger.warning("Failed to track PostHog event '%s': %s", event_name, e)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def shutdown() -> None:
|
|
146
|
+
"""Shutdown PostHog client and flush any pending events."""
|
|
147
|
+
global _posthog_client
|
|
148
|
+
|
|
149
|
+
if _posthog_client is not None:
|
|
150
|
+
try:
|
|
151
|
+
_posthog_client.shutdown()
|
|
152
|
+
logger.debug("PostHog client shutdown successfully")
|
|
153
|
+
except Exception as e:
|
|
154
|
+
logger.warning("Error shutting down PostHog: %s", e)
|
|
155
|
+
finally:
|
|
156
|
+
_posthog_client = None
|