Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions backend/api/onboarding_utils/onboarding_completion_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from services.persona.facebook.facebook_persona_scheduler import schedule_facebook_persona_generation
from services.oauth_token_monitoring_service import create_oauth_monitoring_tasks
from services.onboarding.unified_oauth_validator import UnifiedOAuthValidator
from services.platform_insights_monitoring_service import create_platform_insights_task

class OnboardingCompletionService:
"""Service for handling onboarding completion logic."""
Expand Down Expand Up @@ -84,6 +85,31 @@ async def complete_onboarding(self, current_user: Dict[str, Any]) -> Dict[str, A
# Non-critical: log but don't fail onboarding completion
logger.warning(f"Failed to create OAuth token monitoring tasks for user {user_id}: {e}")


# Create platform insights tasks (GSC/Bing) after onboarding completion
try:
from services.database import SessionLocal
db = SessionLocal()
try:
connection_summary = self.oauth_validator.get_connection_summary(user_id)
platform_ids = [p.get('provider') for p in connection_summary.get('platforms', []) if p.get('status') == 'active']
created_platform_tasks = []
for platform in platform_ids:
if platform in {'gsc', 'bing'}:
task_res = create_platform_insights_task(
user_id=user_id,
platform=platform,
site_url=None,
db=db
)
if task_res.get('success'):
created_platform_tasks.append(task_res.get('task_id'))
logger.info(f"Created/verified platform insights tasks for user {user_id}: {created_platform_tasks}")
finally:
db.close()
except Exception as e:
logger.warning(f"Failed to create platform insights tasks for user {user_id}: {e}")

# Create website analysis tasks for user's website and competitors
try:
from services.database import SessionLocal
Expand Down
54 changes: 54 additions & 0 deletions backend/routers/gsc_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@

# Initialize GSC service (for backward compatibility)
from services.gsc_service import GSCService
from services.gsc_task_report_service import GSCTaskReportService
gsc_service = GSCService()


Expand Down Expand Up @@ -68,6 +69,19 @@ class GSCCachedOpportunitiesResponse(BaseModel):
opportunities: List[Dict[str, Any]]
generated_from_cache: bool


class GSCTaskReportResponse(BaseModel):
connected: bool
site_url: Optional[str] = None
generated_at: Optional[str] = None
sections: List[Dict[str, Any]]
google_query_templates: List[str]


class GSCRunTaskRequest(BaseModel):
task_key: str
site_url: Optional[str] = None

@router.get("/auth/url")
async def get_gsc_auth_url(request: Request, user: dict = Depends(get_current_user)):
"""
Expand Down Expand Up @@ -508,3 +522,43 @@ async def gsc_health_check():
except Exception as e:
logger.error(f"GSC health check failed: {e}")
raise HTTPException(status_code=500, detail="GSC service unhealthy")


@router.get("/task-reports", response_model=GSCTaskReportResponse)
async def get_gsc_task_reports(
site_url: Optional[str] = Query(None, description="Optional GSC site URL"),
user: dict = Depends(get_current_user)
):
"""Get issue 1-4 task sections for onboarding and SEO dashboard widgets."""
try:
user_id = user.get('id')
if not user_id:
raise HTTPException(status_code=400, detail="User ID not found")

service = GSCTaskReportService()
return service.build_task_report(user_id=str(user_id), site_url=site_url)
except Exception as e:
logger.error(f"Error getting GSC task reports: {e}")
raise HTTPException(status_code=500, detail=f"Error getting task reports: {str(e)}")


@router.post("/task-reports/run")
async def run_gsc_task_report(
request: GSCRunTaskRequest,
user: dict = Depends(get_current_user)
):
"""Run one issue task once (onboarding learn mode)."""
try:
user_id = user.get('id')
if not user_id:
raise HTTPException(status_code=400, detail="User ID not found")

service = GSCTaskReportService()
return service.run_single_task(
user_id=str(user_id),
task_key=request.task_key,
site_url=request.site_url
)
except Exception as e:
logger.error(f"Error running GSC task report: {e}")
raise HTTPException(status_code=500, detail=f"Error running task report: {str(e)}")
41 changes: 41 additions & 0 deletions backend/services/gsc_query_request_shapes_tests.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
from services.gsc_service import GSCService


class MinimalGSCService(GSCService):
def __init__(self):
# Skip DB/table init for pure request-shape tests.
pass


def test_build_query_request_uses_documented_bounds_and_type():
svc = MinimalGSCService()

req = svc._build_search_analytics_request(
start_date="2026-01-01",
end_date="2026-01-31",
dimensions=["query"],
row_limit=100000,
start_row=-10,
)

assert req["startDate"] == "2026-01-01"
assert req["endDate"] == "2026-01-31"
assert req["type"] == "web"
assert req["dimensions"] == ["query"]
assert req["startRow"] == 0
assert req["rowLimit"] == 25000


def test_build_overall_request_omits_dimensions_for_aggregate_totals():
svc = MinimalGSCService()

req = svc._build_search_analytics_request(
start_date="2026-01-01",
end_date="2026-01-31",
dimensions=None,
row_limit=1,
)

assert "dimensions" not in req
assert req["rowLimit"] == 1
assert req["type"] == "web"
69 changes: 52 additions & 17 deletions backend/services/gsc_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@

class GSCService:
"""Service for Google Search Console integration."""
DEFAULT_SEARCH_TYPE = 'web'
MAX_ROW_LIMIT = 25000

def __init__(self):
"""Initialize GSC service with database connection."""
Expand Down Expand Up @@ -342,11 +344,12 @@ def get_search_analytics(self, user_id: str, site_url: str,
return {'error': 'Authentication failed', 'rows': [], 'rowCount': 0}

# Step 1: Verify data presence first (as per GSC API documentation)
verification_request = {
'startDate': start_date,
'endDate': end_date,
'dimensions': ['date'] # Only date dimension for verification
}
verification_request = self._build_search_analytics_request(
start_date=start_date,
end_date=end_date,
dimensions=['date'],
row_limit=self.MAX_ROW_LIMIT,
)

logger.info(f"GSC Data verification request for user {user_id}: {verification_request}")

Expand All @@ -371,12 +374,12 @@ def get_search_analytics(self, user_id: str, site_url: str,
return {'error': f'Data verification failed: {str(verification_error)}', 'rows': [], 'rowCount': 0}

# Step 2: Get overall metrics (no dimensions)
request = {
'startDate': start_date,
'endDate': end_date,
'dimensions': [], # No dimensions for overall metrics
'rowLimit': 1000
}
request = self._build_search_analytics_request(
start_date=start_date,
end_date=end_date,
dimensions=None, # Aggregated totals (no dimensions)
row_limit=1,
)

logger.info(f"GSC API request for user {user_id}: {request}")

Expand All @@ -392,12 +395,12 @@ def get_search_analytics(self, user_id: str, site_url: str,
return {'error': str(api_error), 'rows': [], 'rowCount': 0}

# Step 3: Get query-level data for insights (as per documentation)
query_request = {
'startDate': start_date,
'endDate': end_date,
'dimensions': ['query'], # Get query-level data
'rowLimit': 1000
}
query_request = self._build_search_analytics_request(
start_date=start_date,
end_date=end_date,
dimensions=['query'],
row_limit=self.MAX_ROW_LIMIT,
)

logger.info(f"GSC Query-level request for user {user_id}: {query_request}")

Expand Down Expand Up @@ -458,6 +461,38 @@ def get_search_analytics(self, user_id: str, site_url: str,
except Exception as e:
logger.error(f"Error getting search analytics for user {user_id}: {e}")
raise

def _build_search_analytics_request(
self,
start_date: str,
end_date: str,
dimensions: Optional[List[str]] = None,
row_limit: Optional[int] = None,
start_row: int = 0,
search_type: Optional[str] = None,
) -> Dict[str, Any]:
"""Build a GSC Search Analytics request body aligned with API documentation.

Notes:
- `dimensions` is optional; omit it entirely for aggregated totals.
- `rowLimit` max is 25,000 per API call.
- `type` defaults to `web` when not specified.
"""
request: Dict[str, Any] = {
'startDate': start_date,
'endDate': end_date,
'type': search_type or self.DEFAULT_SEARCH_TYPE,
'startRow': max(start_row, 0),
}

if dimensions:
request['dimensions'] = dimensions

if row_limit is not None:
bounded_row_limit = max(1, min(int(row_limit), self.MAX_ROW_LIMIT))
request['rowLimit'] = bounded_row_limit

return request

def get_sitemaps(self, user_id: str, site_url: str) -> List[Dict[str, Any]]:
"""Get sitemaps from GSC."""
Expand Down
Loading