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
57 changes: 55 additions & 2 deletions pkg/service/browser.go
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,8 @@ func (s *BrowserService) Render(ctx context.Context, url string, optionFuncs ...
scrollForElements(opts.timeBetweenScrolls),
waitForDuration(time.Second),
waitForReady(browserCtx, opts.timeout),
resizeViewportForFullHeight(opts), // Resize after all content is loaded and ready
waitForReady(browserCtx, opts.timeout), // Wait for readiness again after viewport resize
opts.printer.action(fileChan, opts),
}
span.AddEvent("actions created")
Expand Down Expand Up @@ -728,7 +730,7 @@ type pngPrinter struct {
fullHeight bool
}

func (p *pngPrinter) action(dst chan []byte, _ *renderingOptions) chromedp.Action {
func (p *pngPrinter) action(dst chan []byte, opts *renderingOptions) chromedp.Action {
return chromedp.ActionFunc(func(ctx context.Context) error {
tracer := tracer(ctx)
ctx, span := tracer.Start(ctx, "pngPrinter.action",
Expand All @@ -739,7 +741,10 @@ func (p *pngPrinter) action(dst chan []byte, _ *renderingOptions) chromedp.Actio

output, err := page.CaptureScreenshot().
WithFormat(page.CaptureScreenshotFormatPng).
WithCaptureBeyondViewport(p.fullHeight).
// We don't want to use this option: it doesn't take a full window screenshot,
// rather it takes a screenshot including content that bleeds outside the viewport (e.g. something 110vh tall).
// Instead, we change the viewport height to match the content height.
WithCaptureBeyondViewport(false).
Do(ctx)
if err != nil {
span.SetStatus(codes.Error, err.Error())
Expand Down Expand Up @@ -826,6 +831,54 @@ func setCookies(cookies []*network.SetCookieParams) chromedp.Action {
})
}

func resizeViewportForFullHeight(opts *renderingOptions) chromedp.Action {
return chromedp.ActionFunc(func(ctx context.Context) error {
// Only resize for PNG printers with fullHeight enabled
pngPrinter, ok := opts.printer.(*pngPrinter)
if !ok || !pngPrinter.fullHeight {
return nil // Skip for non-PNG or non-fullHeight screenshots
}

tracer := tracer(ctx)
ctx, span := tracer.Start(ctx, "resizeViewportForFullHeight")
defer span.End()

var scrollHeight int
err := chromedp.Evaluate(`document.body.scrollHeight`, &scrollHeight).Do(ctx)
if err != nil {
span.SetStatus(codes.Error, "failed to get scroll height: "+err.Error())
return fmt.Errorf("failed to get scroll height: %w", err)
}

// Only resize if the page is actually taller than the current viewport
if scrollHeight > opts.viewportHeight {
span.AddEvent("resizing viewport for full height capture",
trace.WithAttributes(
attribute.Int("originalHeight", opts.viewportHeight),
attribute.Int("newHeight", scrollHeight),
))

// Determine orientation from options
orientation := chromedp.EmulatePortrait
if opts.landscape {
orientation = chromedp.EmulateLandscape
}

err = chromedp.EmulateViewport(int64(opts.viewportWidth), int64(scrollHeight), orientation).Do(ctx)
if err != nil {
span.SetStatus(codes.Error, "failed to resize viewport: "+err.Error())
return fmt.Errorf("failed to resize viewport for full height: %w", err)
}

span.SetStatus(codes.Ok, "viewport resized successfully")
} else {
span.AddEvent("no viewport resize needed", trace.WithAttributes(attribute.Int("pageHeight", scrollHeight)))
}

return nil
})
}

func scrollForElements(timeBetweenScrolls time.Duration) chromedp.Action {
return chromedp.ActionFunc(func(ctx context.Context) error {
tracer := tracer(ctx)
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
41 changes: 38 additions & 3 deletions tests/acceptance/rendering_grafana_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,7 @@ func TestRenderingGrafana(t *testing.T) {
}
})

t.Run("render very long prometheus dashboard as PDF", func(t *testing.T) {
t.Run("render very long prometheus dashboard", func(t *testing.T) {
t.Parallel()

net, err := network.New(t.Context())
Expand All @@ -305,7 +305,7 @@ func TestRenderingGrafana(t *testing.T) {
WithEnv("GF_RENDERING_CALLBACK_URL", "http://grafana:3000/"),
WithEnv("GF_RENDERING_RENDERER_TOKEN", rendererAuthToken))

t.Run("render many pages", func(t *testing.T) {
t.Run("render PDF of many pages", func(t *testing.T) {
t.Parallel()

req, err := http.NewRequestWithContext(t.Context(), http.MethodGet, svc.HTTPEndpoint+"/render", nil)
Expand Down Expand Up @@ -340,7 +340,7 @@ func TestRenderingGrafana(t *testing.T) {
"first 3": "1-3",
"1 and 3": "1, 3",
} {
t.Run("print with pageRanges="+name, func(t *testing.T) {
t.Run("print PDF with pageRanges="+name, func(t *testing.T) {
t.Parallel()

req, err := http.NewRequestWithContext(t.Context(), http.MethodGet, svc.HTTPEndpoint+"/render", nil)
Expand Down Expand Up @@ -368,6 +368,41 @@ func TestRenderingGrafana(t *testing.T) {
}
})
}

t.Run("render many pages as PNG with full height", func(t *testing.T) {
t.Parallel()

for _, isLandscape := range []bool{true, false} {
t.Run("landscape="+fmt.Sprintf("%v", isLandscape), func(t *testing.T) {
t.Parallel()

req, err := http.NewRequestWithContext(t.Context(), http.MethodGet, svc.HTTPEndpoint+"/render", nil)
require.NoError(t, err, "could not construct HTTP request to Grafana")
req.Header.Set("Accept", "image/png")
req.Header.Set("X-Auth-Token", "-")
query := req.URL.Query()
query.Set("url", "http://grafana:3000/d/very-long-prometheus-dashboard?render=1&from=1699333200000&to=1699344000000&kiosk=true")
query.Set("encoding", "png")
query.Set("renderKey", renderKey)
query.Set("domain", "grafana")
query.Set("height", "-1")
query.Set("landscape", fmt.Sprintf("%v", isLandscape))
req.URL.RawQuery = query.Encode()

resp, err := http.DefaultClient.Do(req)
require.NoError(t, err, "could not send HTTP request to Grafana")
require.Equal(t, http.StatusOK, resp.StatusCode, "unexpected HTTP status code from Grafana")

body := ReadBody(t, resp.Body)
image := ReadRGBA(t, body)
fixture := fmt.Sprintf("render-very-long-prometheus-dashboard-full-height-landscape-%v.png", isLandscape)
fixtureImg := ReadFixtureRGBA(t, fixture)
if !AssertPixelDifference(t, fixtureImg, image, 125_000) { // this is a very long image, so data may be off by a little bit
UpdateFixtureIfEnabled(t, fixture, body)
}
})
}
})
})

t.Run("render panel dashboards as PNG", func(t *testing.T) {
Expand Down
Loading