Major additions: - Multi-state expansion: states table, StateSelector, NationalDashboard, StateHeatmap, CrossStateCompare - Orchestrator services: trace service, error taxonomy, retry manager, proxy rotator - Discovery system: dutchie discovery service, geo validation, city seeding scripts - Analytics infrastructure: analytics v2 routes, brand/pricing/stores intelligence pages - Local development: setup-local.sh starts all 5 services (postgres, backend, cannaiq, findadispo, findagram) - Migrations 037-056: crawler profiles, states, analytics indexes, worker metadata Frontend pages added: - Discovery, ChainsDashboard, IntelligenceBrands, IntelligencePricing, IntelligenceStores - StateHeatmap, CrossStateCompare, SyncInfoPanel Components added: - StateSelector, OrchestratorTraceModal, WorkflowStepper 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
595 lines
12 KiB
Markdown
595 lines
12 KiB
Markdown
# Analytics V2 API Examples
|
|
|
|
## Overview
|
|
|
|
All endpoints are prefixed with `/api/analytics/v2`
|
|
|
|
### Filtering Options
|
|
|
|
**Time Windows:**
|
|
- `?window=7d` - Last 7 days
|
|
- `?window=30d` - Last 30 days (default)
|
|
- `?window=90d` - Last 90 days
|
|
|
|
**Legal Type Filtering:**
|
|
- `?legalType=recreational` - Recreational states only
|
|
- `?legalType=medical_only` - Medical-only states (not recreational)
|
|
- `?legalType=no_program` - States with no cannabis program
|
|
|
|
---
|
|
|
|
## 1. Price Analytics
|
|
|
|
### GET /price/product/:id
|
|
|
|
Get price trends for a specific store product.
|
|
|
|
**Request:**
|
|
```bash
|
|
GET /api/analytics/v2/price/product/12345?window=30d
|
|
```
|
|
|
|
**Response:**
|
|
```json
|
|
{
|
|
"store_product_id": 12345,
|
|
"product_name": "Blue Dream 3.5g",
|
|
"brand_name": "Cookies",
|
|
"category": "Flower",
|
|
"dispensary_id": 101,
|
|
"dispensary_name": "Green Leaf Dispensary",
|
|
"state_code": "AZ",
|
|
"data_points": [
|
|
{
|
|
"date": "2024-11-06",
|
|
"price_rec": 45.00,
|
|
"price_med": 40.00,
|
|
"price_rec_special": null,
|
|
"price_med_special": null,
|
|
"is_on_special": false
|
|
},
|
|
{
|
|
"date": "2024-11-07",
|
|
"price_rec": 42.00,
|
|
"price_med": 38.00,
|
|
"price_rec_special": null,
|
|
"price_med_special": null,
|
|
"is_on_special": false
|
|
}
|
|
],
|
|
"summary": {
|
|
"current_price": 42.00,
|
|
"min_price": 40.00,
|
|
"max_price": 48.00,
|
|
"avg_price": 43.50,
|
|
"price_change_count": 3,
|
|
"volatility_percent": 8.2
|
|
}
|
|
}
|
|
```
|
|
|
|
### GET /price/rec-vs-med
|
|
|
|
Get recreational vs medical-only price comparison by category.
|
|
|
|
**Request:**
|
|
```bash
|
|
GET /api/analytics/v2/price/rec-vs-med?category=Flower
|
|
```
|
|
|
|
**Response:**
|
|
```json
|
|
[
|
|
{
|
|
"category": "Flower",
|
|
"rec_avg": 38.50,
|
|
"rec_median": 35.00,
|
|
"med_avg": 42.00,
|
|
"med_median": 40.00
|
|
},
|
|
{
|
|
"category": "Concentrates",
|
|
"rec_avg": 45.00,
|
|
"rec_median": 42.00,
|
|
"med_avg": 48.00,
|
|
"med_median": 45.00
|
|
}
|
|
]
|
|
```
|
|
|
|
---
|
|
|
|
## 2. Brand Analytics
|
|
|
|
### GET /brand/:name/penetration
|
|
|
|
Get brand penetration metrics with state breakdown.
|
|
|
|
**Request:**
|
|
```bash
|
|
GET /api/analytics/v2/brand/Cookies/penetration?window=30d
|
|
```
|
|
|
|
**Response:**
|
|
```json
|
|
{
|
|
"brand_name": "Cookies",
|
|
"total_dispensaries": 125,
|
|
"total_skus": 450,
|
|
"avg_skus_per_dispensary": 3.6,
|
|
"states_present": ["AZ", "CA", "CO", "NV", "MI"],
|
|
"state_breakdown": [
|
|
{
|
|
"state_code": "CA",
|
|
"state_name": "California",
|
|
"legal_type": "recreational",
|
|
"dispensary_count": 45,
|
|
"sku_count": 180,
|
|
"avg_skus_per_dispensary": 4.0,
|
|
"market_share_percent": 12.5
|
|
},
|
|
{
|
|
"state_code": "AZ",
|
|
"state_name": "Arizona",
|
|
"legal_type": "recreational",
|
|
"dispensary_count": 32,
|
|
"sku_count": 128,
|
|
"avg_skus_per_dispensary": 4.0,
|
|
"market_share_percent": 15.2
|
|
}
|
|
],
|
|
"penetration_trend": [
|
|
{
|
|
"date": "2024-11-01",
|
|
"dispensary_count": 120,
|
|
"new_dispensaries": 0,
|
|
"dropped_dispensaries": 0
|
|
},
|
|
{
|
|
"date": "2024-11-08",
|
|
"dispensary_count": 123,
|
|
"new_dispensaries": 3,
|
|
"dropped_dispensaries": 0
|
|
},
|
|
{
|
|
"date": "2024-11-15",
|
|
"dispensary_count": 125,
|
|
"new_dispensaries": 2,
|
|
"dropped_dispensaries": 0
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
### GET /brand/:name/rec-vs-med
|
|
|
|
Get brand presence in recreational vs medical-only states.
|
|
|
|
**Request:**
|
|
```bash
|
|
GET /api/analytics/v2/brand/Cookies/rec-vs-med
|
|
```
|
|
|
|
**Response:**
|
|
```json
|
|
{
|
|
"brand_name": "Cookies",
|
|
"rec_states_count": 4,
|
|
"rec_states": ["AZ", "CA", "CO", "NV"],
|
|
"rec_dispensary_count": 110,
|
|
"rec_avg_skus": 3.8,
|
|
"med_only_states_count": 2,
|
|
"med_only_states": ["FL", "OH"],
|
|
"med_only_dispensary_count": 15,
|
|
"med_only_avg_skus": 2.5
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 3. Category Analytics
|
|
|
|
### GET /category/:name/growth
|
|
|
|
Get category growth metrics with state breakdown.
|
|
|
|
**Request:**
|
|
```bash
|
|
GET /api/analytics/v2/category/Flower/growth?window=30d
|
|
```
|
|
|
|
**Response:**
|
|
```json
|
|
{
|
|
"category": "Flower",
|
|
"current_sku_count": 5200,
|
|
"current_dispensary_count": 320,
|
|
"avg_price": 38.50,
|
|
"growth_data": [
|
|
{
|
|
"date": "2024-11-01",
|
|
"sku_count": 4800,
|
|
"dispensary_count": 310,
|
|
"avg_price": 39.00
|
|
},
|
|
{
|
|
"date": "2024-11-15",
|
|
"sku_count": 5000,
|
|
"dispensary_count": 315,
|
|
"avg_price": 38.75
|
|
},
|
|
{
|
|
"date": "2024-12-01",
|
|
"sku_count": 5200,
|
|
"dispensary_count": 320,
|
|
"avg_price": 38.50
|
|
}
|
|
],
|
|
"state_breakdown": [
|
|
{
|
|
"state_code": "CA",
|
|
"state_name": "California",
|
|
"legal_type": "recreational",
|
|
"sku_count": 2100,
|
|
"dispensary_count": 145,
|
|
"avg_price": 36.00
|
|
},
|
|
{
|
|
"state_code": "AZ",
|
|
"state_name": "Arizona",
|
|
"legal_type": "recreational",
|
|
"sku_count": 950,
|
|
"dispensary_count": 85,
|
|
"avg_price": 40.00
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
### GET /category/rec-vs-med
|
|
|
|
Get category comparison between recreational and medical-only states.
|
|
|
|
**Request:**
|
|
```bash
|
|
GET /api/analytics/v2/category/rec-vs-med
|
|
```
|
|
|
|
**Response:**
|
|
```json
|
|
[
|
|
{
|
|
"category": "Flower",
|
|
"recreational": {
|
|
"state_count": 15,
|
|
"dispensary_count": 650,
|
|
"sku_count": 12500,
|
|
"avg_price": 35.50,
|
|
"median_price": 32.00
|
|
},
|
|
"medical_only": {
|
|
"state_count": 8,
|
|
"dispensary_count": 220,
|
|
"sku_count": 4200,
|
|
"avg_price": 42.00,
|
|
"median_price": 40.00
|
|
},
|
|
"price_diff_percent": -15.48
|
|
},
|
|
{
|
|
"category": "Concentrates",
|
|
"recreational": {
|
|
"state_count": 15,
|
|
"dispensary_count": 600,
|
|
"sku_count": 8500,
|
|
"avg_price": 42.00,
|
|
"median_price": 40.00
|
|
},
|
|
"medical_only": {
|
|
"state_count": 8,
|
|
"dispensary_count": 200,
|
|
"sku_count": 3100,
|
|
"avg_price": 48.00,
|
|
"median_price": 45.00
|
|
},
|
|
"price_diff_percent": -12.50
|
|
}
|
|
]
|
|
```
|
|
|
|
---
|
|
|
|
## 4. Store Analytics
|
|
|
|
### GET /store/:id/summary
|
|
|
|
Get change summary for a store over a time window.
|
|
|
|
**Request:**
|
|
```bash
|
|
GET /api/analytics/v2/store/101/summary?window=30d
|
|
```
|
|
|
|
**Response:**
|
|
```json
|
|
{
|
|
"dispensary_id": 101,
|
|
"dispensary_name": "Green Leaf Dispensary",
|
|
"state_code": "AZ",
|
|
"window": "30d",
|
|
"products_added": 45,
|
|
"products_dropped": 12,
|
|
"brands_added": ["Alien Labs", "Connected"],
|
|
"brands_dropped": ["House Brand"],
|
|
"price_changes": 156,
|
|
"avg_price_change_percent": 3.2,
|
|
"stock_in_events": 89,
|
|
"stock_out_events": 34,
|
|
"current_product_count": 512,
|
|
"current_in_stock_count": 478
|
|
}
|
|
```
|
|
|
|
### GET /store/:id/events
|
|
|
|
Get recent product change events for a store.
|
|
|
|
**Request:**
|
|
```bash
|
|
GET /api/analytics/v2/store/101/events?window=7d&limit=50
|
|
```
|
|
|
|
**Response:**
|
|
```json
|
|
[
|
|
{
|
|
"store_product_id": 12345,
|
|
"product_name": "Blue Dream 3.5g",
|
|
"brand_name": "Cookies",
|
|
"category": "Flower",
|
|
"event_type": "price_change",
|
|
"event_date": "2024-12-05T14:30:00.000Z",
|
|
"old_value": "45.00",
|
|
"new_value": "42.00"
|
|
},
|
|
{
|
|
"store_product_id": 12346,
|
|
"product_name": "OG Kush 1g",
|
|
"brand_name": "Alien Labs",
|
|
"category": "Flower",
|
|
"event_type": "added",
|
|
"event_date": "2024-12-04T10:00:00.000Z",
|
|
"old_value": null,
|
|
"new_value": null
|
|
},
|
|
{
|
|
"store_product_id": 12300,
|
|
"product_name": "Sour Diesel Cart",
|
|
"brand_name": "Select",
|
|
"category": "Vaporizers",
|
|
"event_type": "stock_out",
|
|
"event_date": "2024-12-03T16:45:00.000Z",
|
|
"old_value": "true",
|
|
"new_value": "false"
|
|
}
|
|
]
|
|
```
|
|
|
|
---
|
|
|
|
## 5. State Analytics
|
|
|
|
### GET /state/:code/summary
|
|
|
|
Get market summary for a specific state with rec/med breakdown.
|
|
|
|
**Request:**
|
|
```bash
|
|
GET /api/analytics/v2/state/AZ/summary
|
|
```
|
|
|
|
**Response:**
|
|
```json
|
|
{
|
|
"state_code": "AZ",
|
|
"state_name": "Arizona",
|
|
"legal_status": {
|
|
"recreational_legal": true,
|
|
"rec_year": 2020,
|
|
"medical_legal": true,
|
|
"med_year": 2010
|
|
},
|
|
"coverage": {
|
|
"dispensary_count": 145,
|
|
"product_count": 18500,
|
|
"brand_count": 320,
|
|
"category_count": 12,
|
|
"snapshot_count": 2450000,
|
|
"last_crawl_at": "2024-12-06T02:30:00.000Z"
|
|
},
|
|
"pricing": {
|
|
"avg_price": 42.50,
|
|
"median_price": 38.00,
|
|
"min_price": 5.00,
|
|
"max_price": 250.00
|
|
},
|
|
"top_categories": [
|
|
{ "category": "Flower", "count": 5200 },
|
|
{ "category": "Concentrates", "count": 3800 },
|
|
{ "category": "Vaporizers", "count": 2950 },
|
|
{ "category": "Edibles", "count": 2400 },
|
|
{ "category": "Pre-Rolls", "count": 1850 }
|
|
],
|
|
"top_brands": [
|
|
{ "brand": "Cookies", "count": 450 },
|
|
{ "brand": "Alien Labs", "count": 380 },
|
|
{ "brand": "Connected", "count": 320 },
|
|
{ "brand": "Stiiizy", "count": 290 },
|
|
{ "brand": "Raw Garden", "count": 275 }
|
|
]
|
|
}
|
|
```
|
|
|
|
### GET /state/legal-breakdown
|
|
|
|
Get breakdown by legal status (recreational, medical-only, no program).
|
|
|
|
**Request:**
|
|
```bash
|
|
GET /api/analytics/v2/state/legal-breakdown
|
|
```
|
|
|
|
**Response:**
|
|
```json
|
|
{
|
|
"recreational_states": {
|
|
"count": 24,
|
|
"dispensary_count": 850,
|
|
"product_count": 125000,
|
|
"snapshot_count": 15000000,
|
|
"states": [
|
|
{ "code": "CA", "name": "California", "dispensary_count": 250 },
|
|
{ "code": "CO", "name": "Colorado", "dispensary_count": 150 },
|
|
{ "code": "AZ", "name": "Arizona", "dispensary_count": 145 },
|
|
{ "code": "MI", "name": "Michigan", "dispensary_count": 120 }
|
|
]
|
|
},
|
|
"medical_only_states": {
|
|
"count": 18,
|
|
"dispensary_count": 320,
|
|
"product_count": 45000,
|
|
"snapshot_count": 5000000,
|
|
"states": [
|
|
{ "code": "FL", "name": "Florida", "dispensary_count": 120 },
|
|
{ "code": "OH", "name": "Ohio", "dispensary_count": 85 },
|
|
{ "code": "PA", "name": "Pennsylvania", "dispensary_count": 75 }
|
|
]
|
|
},
|
|
"no_program_states": {
|
|
"count": 9,
|
|
"states": [
|
|
{ "code": "ID", "name": "Idaho" },
|
|
{ "code": "WY", "name": "Wyoming" },
|
|
{ "code": "KS", "name": "Kansas" }
|
|
]
|
|
}
|
|
}
|
|
```
|
|
|
|
### GET /state/recreational
|
|
|
|
Get list of recreational state codes.
|
|
|
|
**Request:**
|
|
```bash
|
|
GET /api/analytics/v2/state/recreational
|
|
```
|
|
|
|
**Response:**
|
|
```json
|
|
{
|
|
"legal_type": "recreational",
|
|
"states": ["AK", "AZ", "CA", "CO", "CT", "DE", "IL", "MA", "MD", "ME", "MI", "MN", "MO", "MT", "NJ", "NM", "NV", "NY", "OH", "OR", "RI", "VA", "VT", "WA"],
|
|
"count": 24
|
|
}
|
|
```
|
|
|
|
### GET /state/medical-only
|
|
|
|
Get list of medical-only state codes (not recreational).
|
|
|
|
**Request:**
|
|
```bash
|
|
GET /api/analytics/v2/state/medical-only
|
|
```
|
|
|
|
**Response:**
|
|
```json
|
|
{
|
|
"legal_type": "medical_only",
|
|
"states": ["AR", "FL", "HI", "LA", "MS", "ND", "NH", "OK", "PA", "SD", "UT", "WV"],
|
|
"count": 12
|
|
}
|
|
```
|
|
|
|
### GET /state/rec-vs-med-pricing
|
|
|
|
Get rec vs med price comparison by category.
|
|
|
|
**Request:**
|
|
```bash
|
|
GET /api/analytics/v2/state/rec-vs-med-pricing?category=Flower
|
|
```
|
|
|
|
**Response:**
|
|
```json
|
|
[
|
|
{
|
|
"category": "Flower",
|
|
"recreational": {
|
|
"state_count": 15,
|
|
"product_count": 12500,
|
|
"avg_price": 35.50,
|
|
"median_price": 32.00
|
|
},
|
|
"medical_only": {
|
|
"state_count": 8,
|
|
"product_count": 5200,
|
|
"avg_price": 42.00,
|
|
"median_price": 40.00
|
|
},
|
|
"price_diff_percent": -15.48
|
|
}
|
|
]
|
|
```
|
|
|
|
---
|
|
|
|
## How These Endpoints Support Portals
|
|
|
|
### Brand Portal Use Cases
|
|
|
|
1. **Track brand penetration**: Use `/brand/:name/penetration` to see how many stores carry the brand
|
|
2. **Compare rec vs med markets**: Use `/brand/:name/rec-vs-med` to understand footprint by legal status
|
|
3. **Identify expansion opportunities**: Use `/state/coverage-gaps` to find underserved markets
|
|
4. **Monitor pricing**: Use `/price/brand/:brand` to track pricing by state
|
|
|
|
### Buyer Portal Use Cases
|
|
|
|
1. **Compare stores**: Use `/store/:id/summary` to see activity levels
|
|
2. **Track price changes**: Use `/store/:id/events` to monitor competitor pricing
|
|
3. **Analyze categories**: Use `/category/:name/growth` to identify trending products
|
|
4. **State-level insights**: Use `/state/:code/summary` for market overview
|
|
|
|
---
|
|
|
|
## Time Window Filtering
|
|
|
|
All time-based endpoints support the `window` query parameter:
|
|
|
|
| Value | Description |
|
|
|-------|-------------|
|
|
| `7d` | Last 7 days |
|
|
| `30d` | Last 30 days (default) |
|
|
| `90d` | Last 90 days |
|
|
|
|
The window affects:
|
|
- `store_product_snapshots.captured_at` for historical data
|
|
- `store_products.first_seen_at` / `last_seen_at` for product lifecycle
|
|
- `crawl_runs.started_at` for crawl-based metrics
|
|
|
|
---
|
|
|
|
## Rec/Med Segmentation
|
|
|
|
All state-level endpoints automatically segment by:
|
|
|
|
- **Recreational**: `states.recreational_legal = TRUE`
|
|
- **Medical-only**: `states.medical_legal = TRUE AND states.recreational_legal = FALSE`
|
|
- **No program**: Both flags are FALSE or NULL
|
|
|
|
This segmentation appears in:
|
|
- `legal_type` field in responses
|
|
- State breakdown arrays
|
|
- Price comparison endpoints
|