Repository navigation
HomeVal analytics dashboard #139
Description
Goal
Let's build a Quarto report that we can use as a dashboard to help answer some questions about how users are interacting with the HomeVal service.
Log message format
Our dashboard will download and analyze log messages from AWS CloudWatch. Here are some details about the format of those log messages.
Example log message
Here's an example log message for convenience. See the /aws-cloudfront/pinval-prod/ CloudWatch log group for more examples.
{
"date": "2025-08-13",
"time": "20:21:41",
"sc-bytes": "13682",
"c-ip": "000.000.000.000",
"cs-method": "GET",
"cs(Host)": "homeval.cookcountyassessoril.gov",
"cs-uri-stem": "/2025/01011000040000.html",
"sc-status": "200",
"cs(Referer)": "-",
"cs(User-Agent)": "Mozilla/5.0%20(Windows%20NT%2010.0;%20Win64;%20x64;%20rv:141.0)%20Gecko/20100101%20Firefox/141.0",
"cs-uri-query": "-",
"x-host-header": "homeval.cookcountyassessoril.gov",
"cs-protocol": "https",
"cs-bytes": "44",
"time-taken": "0.193",
"x-forwarded-for": "-",
"x-edge-response-result-type": "Miss",
"cs-protocol-version": "HTTP/2.0",
"c-port": "12944",
"time-to-first-byte": "0.167",
"sc-content-type": "text/html",
"sc-content-len": "-"
}Log attribute descriptions
| Attribute | Description |
|---|---|
date |
The date (YYYY-MM-DD) when the request was received by CloudFront. |
time |
The time (HH:MM:SS) when the request was received by CloudFront, in UTC. |
sc-bytes |
The number of bytes returned by CloudFront to the client (response size). |
c-ip |
The IP address of the requesting client. |
cs-method |
The HTTP method used for the request (e.g., GET, POST). |
cs(Host) |
The host header value in the request, usually the CloudFront distribution domain name. |
cs-uri-stem |
The URI path of the requested resource (excluding query string). |
sc-status |
The HTTP status code returned to the client (e.g., 200, 304, 404). |
cs(Referer) |
The Referer header from the request, indicating the previous page or source of the request. |
cs(User-Agent) |
The User-Agent header from the request, identifying the client browser or tool. |
cs-uri-query |
The query string portion of the requested URI, if present; otherwise a dash (-). |
x-host-header |
The original Host header value sent by the client, if different from the CloudFront domain. |
cs-protocol |
The protocol used for the request (e.g., http, https). |
cs-bytes |
The number of bytes received by CloudFront from the client (request size). |
time-taken |
The total time (in seconds) taken to process the request by CloudFront. |
x-forwarded-for |
The X-Forwarded-For header, listing the original client IP (if available); otherwise a dash (-). |
x-edge-response-result-type |
The result of the request at the edge, such as "Hit" (cache hit), "Miss" (cache miss), etc. |
cs-protocol-version |
The HTTP protocol version used for the request (e.g., HTTP/2.0). |
c-port |
The port used by the client to connect to CloudFront. |
time-to-first-byte |
The time (in seconds) taken for CloudFront to serve the first byte of the response to the client. |
sc-content-type |
The Content-Type of the response sent to the client (if available); otherwise a dash (-). |
sc-content-len |
The Content-Length of the response sent to the client (if available); otherwise a dash (-). |
Data extraction
We will want to use the paws API for the CloudWatch GetLogEvents operation to extract log messages. We will also need to query some data from Athena to augment logs.
Cache implementation
- Cache
pinval.vw_assessment_carddataframe to a local parquet file since we expect it to change rarely if ever - Cache log messages by date, with one file corresponding to each date
- Start the date range on 9/4/2025
- Query CloudWatch for logs on any dates between 9/4/2025 and today for which we do not already have a cache file
- Always refresh the cache for the current date so that we can get any new logs for that date; otherwise, skip requests for dates for which we already have cache files
Charts and tables
Report details
For the following charts, it probably would make sense to put charts that use the same source metric together into one tabset so that we group by source metric and allow the user to flip through tabs to show different views on the same source metric. However, I leave the organization of the charts up to your discretion.
Request count by class
- Chart type: Bar
- Key log attributes:
cs-uri-stem- Extract PIN to derive the class from
pinval.vw_assessment_card.char_class
- Extract PIN to derive the class from
- Chart:
- X-Axis: Class
- Y-Axis: Count
Request count by class over time (total)
Same as above, just with stacked bars and the X-Axis set to the date.
Request count by class over time (%)
Same as above, just with stacked bars, the X-Axis set to the date, and the Y-axis value as a proportion that adds up to 100% for each date.
Request count by reassessment year
- Chart type: Bar
- Key log attributes:
cs-uri-stem- Extract year
- Chart:
- X-Axis: Year
- Y-Axis: Count
Request count by reassessment year over time (total)
Same as above, just with stacked bars and the X-Axis set to the date.
Request count by reassessment year over time (%)
Same as above, just with stacked bars, the X-Axis set to the date, and the Y-axis value as a proportion that adds up to 100% for each date.
Request count by report eligibility
- Chart type: Bar
- Key log attributes:
cs-uri-stem- Extract PIN to derive report eligibility from
pinval.vw_assessment_card.is_report_eligible
- Extract PIN to derive report eligibility from
- Chart:
- X-Axis: Eligibility (One for
Eligibleand one forNot Eligible) - Y-Axis: Count
- X-Axis: Eligibility (One for
Request count by report eligibility over time (total)
Same as above, just with stacked bars and the X-Axis set to the date.
Request count by report eligibility over time (%)
Same as above, just with stacked bars, the X-Axis set to the date, and the Y-axis value as a proportion that adds up to 100% for each date.
Request count by report eligibility reason
- Chart type: Bar
- Key log attributes:
cs-uri-stem- Extract PIN to derive report eligibility reason from
pinval.vw_assessment_card.reason_report_ineligible
- Extract PIN to derive report eligibility reason from
- Chart:
- X-Axis: Elibility reason
- Y-Axis: Count
Request count by report eligibility reason over time (total)
Same as above, just with stacked bars and the X-Axis set to the date.
Request count by report eligibility reason over time (%)
Same as above, just with stacked bars, the X-Axis set to the date, and the Y-axis value as a proportion that adds up to 100% for each date.
Request count by township and reassessment year
- Chart type: Bar
- Key log attributes:
cs-uri-stem- Extract PIN and year to derive township from
pinval.vw_assessment_card.meta_township_code
- Extract PIN and year to derive township from
- Chart:
- X-Axis: Township
- Y-Axis: Count
- Group township bars visually by triad
- Not sure how best to represent this in the chart... maybe we could add vertical bars separating triads? I'm open to ideas [NJ note: in addition to the spatial grouping by triad, could also do different colors for each tri]
- Tabset with one tab for each reassessment year
HTTP request and response details
Distribution of HTTP response status codes (all requests)
- Chart type: Multiline
- Key log attributes:
sc-status
- Chart:
- X-axis: Date
- Y-axis: Count
- Line: One per status code (404, 200, etc)
Distribution of HTTP response status codes (PINs only)
- Chart type: Multiline
- Key log attributes:
sc-status- Filter where PIN is not null (derived from
cs-uri-stem)
- Chart:
- X-axis: Date
- Y-axis: Count
- Line: One per status code (404, 200, etc)
Recent request paths that caused errors
- Chart type: Table
- Key log attributes:
sc-statuscs-uri-stem
- Table columns:
- Timestamp (
date+time) - Status (
sc-status) - Path (
cs-uri-stem)
- Timestamp (
- Limit to 100 most recent requests whose status was not 2xx or 3xx
Most common browsers
- Chart type: Bar
- Key log attributes:
cs(User-Agent)- There's a lot of info contained in this field (see example above) so you will need to parse it out
- Chart:
- X-axis: Browser
- Y-axis: Count
- If there are a lot of browsers, this may end up looking better with the axes flipped, or as a table
Most common referer domains (all requests)
I.E. what websites are sending users to our reports; I would expect this to mostly be cookcountyassessoril.gov or homeval.cookcountyassessoril.gov but it would be interesting to see if folks are coming from elsewhere (e.g. social media)
- Chart type: Bar
- Key log attributes:
cs(Referer)- Extract only the domain (i.e. strip everything after the top-level domain)
- Chart:
- X-Axis: Domain
- Y-Axis: Count
- If there are a lot of referers, this chart may end up looking better with the axes flipped, or as a table
Most common referer domains (PINs only)
- Chart type: Bar
- Key log attributes:
cs(Referer)- Extract only the domain (i.e. strip everything after the top-level domain)
- Filter where PIN is not null (based on
cs-uri-stem)
- Chart:
- X-Axis: Domain
- Y-Axis: Count
- If there are a lot of referers, this chart may end up looking better with the axes flipped, or as a table
Cache hit percentage over time (all requests)
- Chart type: Line
- Key log attributes:
x-edge-response-result-type
- Chart:
- X-Axis: Date
- Y-Axis: Cache hit %
Cache hit percentage over time (PINs only)
- Chart type: Line
- Key log attributes:
x-edge-response-result-type- Filter where PIN is not null (based on
cs-uri-stem)
- Chart:
- X-Axis: Date
- Y-Axis: Cache hit %
Activity