Skip to content

Add comprehensive tooltips to every field in cache analysis run panel #88

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
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
27 changes: 24 additions & 3 deletions app/components/CacheAnalysis.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<script setup lang="ts">
import { formatDuration, intervalToDuration } from 'date-fns'
import { getFieldTooltip, getCacheNameTooltip, getForwardReasonTooltip, formatTooltip } from '~/utils/tooltips'

const props = defineProps<{
cacheHeaders: Record<string, string>
Expand Down Expand Up @@ -61,10 +62,10 @@ onUnmounted(() => {
<template>
<div class="container">
<div>
Served by: <strong>{{ cacheAnalysis.servedBy.source }}</strong>
<span :title="formatTooltip(getFieldTooltip('served-by'))">Served by:</span> <strong>{{ cacheAnalysis.servedBy.source }}</strong>
</div>
<div>
CDN node(s): <code>{{ cacheAnalysis.servedBy.cdnNodes }}</code>
<span :title="formatTooltip(getFieldTooltip('cdn-nodes'))">CDN node(s):</span> <code>{{ cacheAnalysis.servedBy.cdnNodes }}</code>
</div>

<hr />
Expand All @@ -88,7 +89,7 @@ onUnmounted(() => {
<!-- This is a bit of a hack to use the pretty <dt> styling but with sections. -->
<!-- I should probably just do something custom instead. -->
<dt class="cache-heading">
<h4>
<h4 :title="formatTooltip(getCacheNameTooltip(cacheName))">
↳ <em>{{ cacheName }}</em> cache
</h4>
</dt>
Expand All @@ -97,6 +98,7 @@ onUnmounted(() => {
<dt
class="data-key"
:class="{ 'key-highlighted': isKeyHovered(`Hit-${cacheIndex}`) }"
:title="formatTooltip(getFieldTooltip('hit'))"
@mouseenter="handleDataKeyHover(`Hit-${cacheIndex}`, parameters.hit)"
@mouseleave="handleDataKeyLeave"
>
Expand All @@ -117,6 +119,7 @@ onUnmounted(() => {
<dt
class="data-key"
:class="{ 'key-highlighted': isKeyHovered(`Forwarded because-${cacheIndex}`) }"
:title="formatTooltip(getFieldTooltip('forwarded-because'))"
@mouseenter="handleDataKeyHover(`Forwarded because-${cacheIndex}`, parameters.fwd)"
@mouseleave="handleDataKeyLeave"
>
Expand All @@ -129,6 +132,7 @@ onUnmounted(() => {
'value-matching': isKeyHovered(`Forwarded because-${cacheIndex}`) && isValueMatching(parameters.fwd),
'value-different': isKeyHovered(`Forwarded because-${cacheIndex}`) && !isValueMatching(parameters.fwd),
}"
:title="formatTooltip(getForwardReasonTooltip(parameters.fwd))"
>
{{ parameters.fwd }}
</dd>
Expand All @@ -138,6 +142,7 @@ onUnmounted(() => {
<dt
class="data-key"
:class="{ 'key-highlighted': isKeyHovered(`Forwarded status-${cacheIndex}`) }"
:title="formatTooltip(getFieldTooltip('forwarded-status'))"
@mouseenter="handleDataKeyHover(`Forwarded status-${cacheIndex}`, parameters['fwd-status'])"
@mouseleave="handleDataKeyLeave"
>
Expand All @@ -159,6 +164,7 @@ onUnmounted(() => {
<dt
class="data-key"
:class="{ 'key-highlighted': isKeyHovered(`TTL-${cacheIndex}`) }"
:title="formatTooltip(getFieldTooltip('ttl'))"
@mouseenter="handleDataKeyHover(`TTL-${cacheIndex}`, parameters.ttl)"
@mouseleave="handleDataKeyLeave"
>
Expand Down Expand Up @@ -187,6 +193,7 @@ onUnmounted(() => {
<dt
class="data-key"
:class="{ 'key-highlighted': isKeyHovered(`Stored the response-${cacheIndex}`) }"
:title="formatTooltip(getFieldTooltip('stored-response'))"
@mouseenter="handleDataKeyHover(`Stored the response-${cacheIndex}`, parameters.stored)"
@mouseleave="handleDataKeyLeave"
>
Expand All @@ -208,6 +215,7 @@ onUnmounted(() => {
<dt
class="data-key"
:class="{ 'key-highlighted': isKeyHovered(`Collapsed w/ other reqs-${cacheIndex}`) }"
:title="formatTooltip(getFieldTooltip('collapsed-requests'))"
@mouseenter="handleDataKeyHover(`Collapsed w/ other reqs-${cacheIndex}`, parameters.collapsed)"
@mouseleave="handleDataKeyLeave"
>
Expand All @@ -229,6 +237,7 @@ onUnmounted(() => {
<dt
class="data-key"
:class="{ 'key-highlighted': isKeyHovered(`Cache key-${cacheIndex}`) }"
:title="formatTooltip(getFieldTooltip('cache-key'))"
@mouseenter="handleDataKeyHover(`Cache key-${cacheIndex}`, parameters.key)"
@mouseleave="handleDataKeyLeave"
>
Expand All @@ -250,6 +259,7 @@ onUnmounted(() => {
<dt
class="data-key"
:class="{ 'key-highlighted': isKeyHovered(`Extra details-${cacheIndex}`) }"
:title="formatTooltip(getFieldTooltip('extra-details'))"
@mouseenter="handleDataKeyHover(`Extra details-${cacheIndex}`, parameters.detail)"
@mouseleave="handleDataKeyLeave"
>
Expand Down Expand Up @@ -280,6 +290,7 @@ onUnmounted(() => {
<dt
class="data-key"
:class="{ 'key-highlighted': isKeyHovered('Cacheable') }"
:title="formatTooltip(getFieldTooltip('cacheable'))"
@mouseenter="handleDataKeyHover('Cacheable', cacheAnalysis.cacheControl.isCacheable)"
@mouseleave="handleDataKeyLeave"
>
Expand All @@ -300,6 +311,7 @@ onUnmounted(() => {
<dt
class="data-key"
:class="{ 'key-highlighted': isKeyHovered('Age') }"
:title="formatTooltip(getFieldTooltip('age'))"
@mouseenter="handleDataKeyHover('Age', cacheAnalysis.cacheControl.age)"
@mouseleave="handleDataKeyLeave"
>
Expand Down Expand Up @@ -328,6 +340,7 @@ onUnmounted(() => {
<dt
class="data-key"
:class="{ 'key-highlighted': isKeyHovered('Date') }"
:title="formatTooltip(getFieldTooltip('date'))"
@mouseenter="handleDataKeyHover('Date', cacheAnalysis.cacheControl.date)"
@mouseleave="handleDataKeyLeave"
>
Expand Down Expand Up @@ -355,6 +368,7 @@ onUnmounted(() => {
<dt
class="data-key"
:class="{ 'key-highlighted': isKeyHovered('ETag') }"
:title="formatTooltip(getFieldTooltip('etag'))"
@mouseenter="handleDataKeyHover('ETag', cacheAnalysis.cacheControl.etag)"
@mouseleave="handleDataKeyLeave"
>
Expand All @@ -376,6 +390,7 @@ onUnmounted(() => {
<dt
class="data-key"
:class="{ 'key-highlighted': isKeyHovered('Expires at') }"
:title="formatTooltip(getFieldTooltip('expires-at'))"
@mouseenter="handleDataKeyHover('Expires at', cacheAnalysis.cacheControl.expiresAt)"
@mouseleave="handleDataKeyLeave"
>
Expand Down Expand Up @@ -403,6 +418,7 @@ onUnmounted(() => {
<dt
class="data-key"
:class="{ 'key-highlighted': isKeyHovered('TTL (browser)') }"
:title="formatTooltip(getFieldTooltip('ttl-browser'))"
@mouseenter="handleDataKeyHover('TTL (browser)', cacheAnalysis.cacheControl.ttl)"
@mouseleave="handleDataKeyLeave"
>
Expand Down Expand Up @@ -436,6 +452,7 @@ onUnmounted(() => {
<dt
class="data-key"
:class="{ 'key-highlighted': isKeyHovered('TTL (CDN)') }"
:title="formatTooltip(getFieldTooltip('ttl-cdn'))"
@mouseenter="handleDataKeyHover('TTL (CDN)', cacheAnalysis.cacheControl.cdnTtl)"
@mouseleave="handleDataKeyLeave"
>
Expand Down Expand Up @@ -468,6 +485,7 @@ onUnmounted(() => {
<dt
class="data-key"
:class="{ 'key-highlighted': isKeyHovered('TTL (Netlify CDN)') }"
:title="formatTooltip(getFieldTooltip('ttl-netlify-cdn'))"
@mouseenter="handleDataKeyHover('TTL (Netlify CDN)', cacheAnalysis.cacheControl.netlifyCdnTtl)"
@mouseleave="handleDataKeyLeave"
>
Expand Down Expand Up @@ -496,6 +514,7 @@ onUnmounted(() => {
<dt
class="data-key"
:class="{ 'key-highlighted': isKeyHovered('Vary') }"
:title="formatTooltip(getFieldTooltip('vary'))"
@mouseenter="handleDataKeyHover('Vary', cacheAnalysis.cacheControl.vary)"
@mouseleave="handleDataKeyLeave"
>
Expand All @@ -517,6 +536,7 @@ onUnmounted(() => {
<dt
class="data-key"
:class="{ 'key-highlighted': isKeyHovered('Netlify-Vary') }"
:title="formatTooltip(getFieldTooltip('netlify-vary'))"
@mouseenter="handleDataKeyHover('Netlify-Vary', cacheAnalysis.cacheControl.netlifyVary)"
@mouseleave="handleDataKeyLeave"
>
Expand All @@ -538,6 +558,7 @@ onUnmounted(() => {
<dt
class="data-key"
:class="{ 'key-highlighted': isKeyHovered('Revalidation') }"
:title="formatTooltip(getFieldTooltip('revalidation'))"
@mouseenter="handleDataKeyHover('Revalidation', cacheAnalysis.cacheControl.revalidate)"
@mouseleave="handleDataKeyLeave"
>
Expand Down
97 changes: 97 additions & 0 deletions app/utils/tooltips.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { describe, it, expect } from 'vitest'
import { getCacheNameTooltip, getForwardReasonTooltip, getFieldTooltip, formatTooltip } from '~/utils/tooltips'

describe('tooltip utilities', () => {
describe('getCacheNameTooltip', () => {
it('returns specific tooltip for Netlify Edge', () => {
const tooltip = getCacheNameTooltip('Netlify Edge')
expect(tooltip.text).toContain('Netlify\'s global edge cache layer')
expect(tooltip.url).toBe('https://docs.netlify.com/platform/caching/')
})

it('returns specific tooltip for Netlify Durable', () => {
const tooltip = getCacheNameTooltip('Netlify Durable')
expect(tooltip.text).toContain('Netlify\'s persistent cache layer')
expect(tooltip.url).toBe('https://docs.netlify.com/platform/caching/')
})

it('returns specific tooltip for Next.js', () => {
const tooltip = getCacheNameTooltip('Next.js')
expect(tooltip.text).toContain('Next.js application-level caching')
expect(tooltip.url).toBe('https://nextjs.org/docs/app/guides/caching#full-route-cache')
})

it('returns generic tooltip for unknown cache names', () => {
const tooltip = getCacheNameTooltip('Unknown Cache')
expect(tooltip.text).toContain('Third-party cache layer "Unknown Cache"')
expect(tooltip.url).toBeUndefined()
})
})

describe('getForwardReasonTooltip', () => {
it('returns specific tooltip for bypass', () => {
const tooltip = getForwardReasonTooltip('bypass')
expect(tooltip.text).toContain('The cache was configured to not store')
})

it('returns specific tooltip for method', () => {
const tooltip = getForwardReasonTooltip('method')
expect(tooltip.text).toContain('The request method was such that')
})

it('returns specific tooltip for uri-miss', () => {
const tooltip = getForwardReasonTooltip('uri-miss')
expect(tooltip.text).toContain('The cache did not have a stored response for this request URI')
})

it('returns specific tooltip for vary-miss', () => {
const tooltip = getForwardReasonTooltip('vary-miss')
expect(tooltip.text).toContain('Vary header field(s) differed')
})

it('returns generic tooltip for unknown forward reasons', () => {
const tooltip = getForwardReasonTooltip('unknown-reason')
expect(tooltip.text).toContain('Cache forwarding reason: unknown-reason')
})
})

describe('getFieldTooltip', () => {
it('returns specific tooltip for served-by', () => {
const tooltip = getFieldTooltip('served-by')
expect(tooltip.text).toContain('The service or component that ultimately served')
})

it('returns specific tooltip for cdn-nodes', () => {
const tooltip = getFieldTooltip('cdn-nodes')
expect(tooltip.text).toContain('The specific CDN node(s) that handled this request')
})

it('returns specific tooltip for netlify-vary with URL', () => {
const tooltip = getFieldTooltip('netlify-vary')
expect(tooltip.text).toContain('Netlify-specific header variations')
expect(tooltip.url).toBe('https://docs.netlify.com/platform/caching/#cache-key-variation')
})

it('returns generic tooltip for unknown fields', () => {
const tooltip = getFieldTooltip('unknown-field')
expect(tooltip.text).toContain('Information about unknown-field')
})
})

describe('formatTooltip', () => {
it('formats tooltip without URL', () => {
const tooltip = { text: 'Simple tooltip text' }
const formatted = formatTooltip(tooltip)
expect(formatted).toBe('Simple tooltip text')
})

it('formats tooltip with URL', () => {
const tooltip = {
text: 'Tooltip text with link',
url: 'https://example.com',
}
const formatted = formatTooltip(tooltip)
expect(formatted).toBe('Tooltip text with link\n\nLearn more: https://example.com')
})
})
})
Loading