Forums

Articles
Create
cancel
Showing results for 
Search instead for 
Did you mean: 

Display Spaces View Report erroring after upgrade

Kathy Dickason
Contributor
August 24, 2026

I successfully ran the ScriptRunner script: Display Spaces View Report (https://www.scriptrunnerhq.com/help/example-scripts/spaces-view-report-onPrem) in Data Center Confluence 9, but now, after an upgrade to Confluence 10.2.14, the script is erroring on line 9 where it references Tursted RequestFactory (I know this has been deprecated), and I do not have the expertise to fix it.  Here is the script that is erroring.  Can someone please fix this for me? Unfortunately, I do not have any worthwhile knowledge of groovy, so I need the exact fix if possible.

 

import org.joda.time.DateTime

import groovy.json.JsonSlurper

import groovy.xml.MarkupBuilder

import org.joda.time.DateTimeZone

import groovyx.net.http.URIBuilder

import com.atlassian.sal.api.UrlMode

import com.atlassian.sal.api.net.Request

import com.atlassian.sal.api.ApplicationProperties

import com.atlassian.sal.api.net.TrustedRequestFactory

import com.onresolve.scriptrunner.parameters.annotation.Select

import com.onresolve.scriptrunner.parameters.annotation.meta.Option

import com.onresolve.scriptrunner.runner.customisers.PluginModule

// @PluginModule

TrustedRequestFactory trustedRequestFactory

@PluginModule

ApplicationProperties applicationProperties

@Select(

        label = "Space Type",

        description = "Select the <b>'Space Type'</b> you wish to include in your result",

        options = [

                @Option(label = "Global", value = "global"),

                @Option(label = "Personal", value = "personal"),

                @Option(label = "Global & Personal", value = "global,personal"),

        ]

)

String spaceType

@Select(

        label = "Content",

        description = "Select the <b>'Content'</b> you wish to include in your result ",

        options = [

                @Option(label = "Page", value = "page"),

                @Option(label = "Blog", value = "blog"),

                @Option(label = "Page & Blog", value = "page,blog"),

        ]

)

String content

@Select(

        label = "Sort Field",

        description = "This is to set which column you want to sort as <b>'Last Viewed'</b> or <b>'Total Views'</b> in the result.",

        options = [

                @Option(label = "Last Viewed", value = "VIEWED_LAST_DATE"),

                @Option(label = "Total Views", value = "VIEWED_COUNT"),

        ]

)

String sortField

@Select(

        label = "Sort Order",

        description = "This is to set which column you want to sort the <b>'Sort Field'</b> above to either ASC or DESC.",

        options = [

                @Option(label = "Ascending", value = "ASC"),

                @Option(label = "Descending", value = "DESC"),

        ]

)

String sortOrder

// Number of days you want to minus from current date. This is one of parameter (Date Ranges) needed in the rest request.

def days = 31

def toDate = new DateTime( new DateTime() , DateTimeZone.UTC )

def fromDate = toDate.minusDays(days)

// Rest request to get the activityBySpace

def rest = '/rest/confanalytics/1.0/instance/paginated/activityBySpace'

def host = applicationProperties.getBaseUrl(UrlMode.CANONICAL)

def url = new URIBuilder( host )

        .setPath(host + rest)

        .addQueryParam("fromDate", fromDate)

        .addQueryParam("toDate", toDate)

        .addQueryParam("period", "week")

        .addQueryParam("spaceType", spaceType)

        .addQueryParam("content", content)

        .addQueryParam("timezone", "GMT+00:00")

        .addQueryParam("type", "total")

        .addQueryParam("limit", "100")

        .addQueryParam("sortField", sortField)

        .addQueryParam("sortOrder", sortOrder) as String

def request = trustedRequestFactory.createTrustedRequest(Request.MethodType.GET, url)

def access = new URIBuilder(url).host

request.addTrustedTokenAuthentication(access)

try {

    def responseBody = request.execute()

    def result = new JsonSlurper().parseText(responseBody) as Map

    def activityBySpace = result.activityBySpace as List<Map>

    def stringWriter = new StringWriter()

    def build = new MarkupBuilder(stringWriter)

// build the output into a table

    build.table(class: "aui") {

        tbody {

            tr {

                th { p("Space Key") }

                th { p("Space Name") }

                th { p("Last Viewed") }

                th { p("Total Views") }

            }

        }

        activityBySpace.each { space ->

            def spaceLink = "<a href='${space?.link}'>${space?.name}</a>"

            tr {

                td { p(space?.key) }

                td { mkp.yieldUnescaped(spaceLink) }

                td { p(space?.lastViewedAt) }

                td { p(space?.views) }

            }

        }

    }

    stringWriter.toString()

} catch ( Exception e ) {

    log.error("Exception >>>", e)

    return e.message

}


 

1 answer

1 accepted

1 vote
Answer accepted
Evgenii
Community Champion
August 24, 2026

Hi @Kathy Dickason,

The script breaks because Atlassian removed the Trusted Applications framework (TrustedRequestFactory) in Confluence 10. Adaptavist provides a drop-in replacement in ScriptRunner: the OAuthRequestSigner HAPI class, which signs the internal REST call the same way, no credentials needed.

import org.joda.time.DateTime
import groovy.json.JsonSlurper
import groovy.xml.MarkupBuilder
import org.joda.time.DateTimeZone
import groovyx.net.http.URIBuilder
import com.atlassian.sal.api.UrlMode
import com.atlassian.oauth.Request
import com.atlassian.sal.api.ApplicationProperties
import com.adaptavist.hapi.platform.oauth.OAuthRequestSigner
import com.onresolve.scriptrunner.parameters.annotation.Select
import com.onresolve.scriptrunner.parameters.annotation.meta.Option
import com.onresolve.scriptrunner.runner.customisers.PluginModule
import java.net.http.HttpClient
import java.net.http.HttpRequest
import java.net.http.HttpResponse

@PluginModule
ApplicationProperties applicationProperties

@Select(
        label = "Space Type",
        description = "Select the <b>'Space Type'</b> you wish to include in your result",
        options = [
                @Option(label = "Global", value = "global"),
                @Option(label = "Personal", value = "personal"),
                @Option(label = "Global & Personal", value = "global,personal"),
        ]
)
String spaceType

@Select(
        label = "Content",
        description = "Select the <b>'Content'</b> you wish to include in your result ",
        options = [
                @Option(label = "Page", value = "page"),
                @Option(label = "Blog", value = "blog"),
                @Option(label = "Page & Blog", value = "page,blog"),
        ]
)
String content

@Select(
        label = "Sort Field",
        description = "This is to set which column you want to sort as <b>'Last Viewed'</b> or <b>'Total Views'</b> in the result.",
        options = [
                @Option(label = "Last Viewed", value = "VIEWED_LAST_DATE"),
                @Option(label = "Total Views", value = "VIEWED_COUNT"),
        ]
)
String sortField

@Select(
        label = "Sort Order",
        description = "This is to set which column you want to sort the <b>'Sort Field'</b> above to either ASC or DESC.",
        options = [
                @Option(label = "Ascending", value = "ASC"),
                @Option(label = "Descending", value = "DESC"),
        ]
)
String sortOrder

// Number of days you want to minus from current date.
def days = 31
def toDate = new DateTime(new DateTime(), DateTimeZone.UTC)
def fromDate = toDate.minusDays(days)

// Rest request to get the activityBySpace
def rest = '/rest/confanalytics/1.0/instance/paginated/activityBySpace'
def host = applicationProperties.getBaseUrl(UrlMode.CANONICAL)
def url = new URIBuilder(host)
        .setPath(host + rest)
        .addQueryParam("fromDate", fromDate)
        .addQueryParam("toDate", toDate)
        .addQueryParam("period", "week")
        .addQueryParam("spaceType", spaceType)
        .addQueryParam("content", content)
        .addQueryParam("timezone", "GMT+00:00")
        .addQueryParam("type", "total")
        .addQueryParam("limit", "100")
        .addQueryParam("sortField", sortField)
        .addQueryParam("sortOrder", sortOrder) as String

try {
    def request = HttpRequest.newBuilder()
            .uri(OAuthRequestSigner.createOAuthUri(url))
            .header("Content-Type", "application/json")
            .header("Authorization", OAuthRequestSigner.createAuthorizationHeader(url, Request.HttpMethod.GET))
            .GET()
            .build()

    def response = HttpClient.newHttpClient()
            .send(request, HttpResponse.BodyHandlers.ofString())

    if (response.statusCode() >= 400) {
        return "Request failed with status code ${response.statusCode()}: ${response.body()}"
    }

    def result = new JsonSlurper().parseText(response.body()) as Map
    def activityBySpace = result.activityBySpace as List<Map>

    def stringWriter = new StringWriter()
    def build = new MarkupBuilder(stringWriter)

    // build the output into a table
    build.table(class: "aui") {
        tbody {
            tr {
                th { p("Space Key") }
                th { p("Space Name") }
                th { p("Last Viewed") }
                th { p("Total Views") }
            }
            activityBySpace.each { space ->
                def spaceLink = "<a href='${space?.link}'>${space?.name}</a>"
                tr {
                    td { p(space?.key) }
                    td { mkp.yieldUnescaped(spaceLink) }
                    td { p(space?.lastViewedAt ? space.lastViewedAt.toString()[0..9] : "") }
                    td { p(space?.views) }
                }
            }
        }
    }
    stringWriter.toString()
} catch (Exception e) {
    log.warn("Exception >>> ${e.message}")
    return e.message
}

What changed: the TrustedRequestFactory import and its @PluginModule injection are gone, and the request is now built with Java's HttpClient and signed by OAuthRequestSigner (ScriptRunner's official replacement, see the "Breaking Changes" page in the ScriptRunner for Confluence documentation). Everything else, the parameters and the report table, works as before.

If the com.adaptavist.hapi.platform.oauth.OAuthRequestSigner import shows as unresolved, update ScriptRunner to the current version first, this class ships with recent ScriptRunner releases for Confluence 10.

Kathy Dickason
Contributor
August 24, 2026

The script looks great in the editor, but now I'm seeing this error when I attempt to run it.  I know the server has rejected the request, but I'm not sure what to even tell our IT people. Can you help me with this at all?

Request failed with status code 401: oauth_problem=signature_invalid&oauth_signature=jq10HIM8dAwcGU3lMzL8x1%2Bv2An2TwoJKGXmZPrQrXiy8Ew4%2F1mZGywvbCX6TPMuJhkLttVhDdnXPloD7c6HZLpm%2FuizLVJ%2BEaQ0YAhjTazLi4lBF2lTtEeRcv1FCAAy3k7wDeq90pvGV8HHfK%2BdBwI3g7%2Fw0EDhZ7TXdWOxpHBqMIIODlZh94XkFtPkAO%2F5ohrrIljdSa3XDBIKhO00qX4QMGourYZT27Aq46uXkgmAUQcHXcNZ%2Bcr76Hi7SL5u68j2of8TXHmT6jhIrz%2Fduxmw%3D%3D&oauth_signature_base_string=GET%26https%253A%252F%252Fconfluence.bigsafari-gc.com%252Frest%252Fconfanalytics%252F1.0%252Finstance%252Fpaginated%252FactivityBySpace%26content%253Dpage%2526fromDate%253D2026-07-24T13%25253A24%25253A53.473Z%2526limit%253D100%2526oauth_consumer_key%253DConfluence%25253A7714378250%2526oauth_nonce%253Df80c0001-9693-459d-bec2-80eb25f370a1_9897072737351038%2526oauth_signature_method%253DRSA-SHA1%2526oauth_timestamp%253D1787577893%2526oauth_token%253D%2526oauth_version%253D1.0%2526period%253Dweek%2526sortField%253DVIEWED_COUNT%2526sortOrder%253DASC%2526spaceType%253Dglobal%2526timezone%253DGMT%25252B00%25253A00%2526toDate%253D2026-08-24T13%25253A24%25253A53.473Z%2526type%253Dtotal%2526xoauth_requestor_id%253Dkdickason%25252540caci.com&oauth_signature_method=RSA-SHA1

Thank you, so much for what you provided above!

Kathy Dickason
Contributor
August 24, 2026

I'm running ScriptRunner 10.16.0.

Kathy Dickason
Contributor
August 24, 2026

Thank you so much for above.  The script looks great in the console, but when I run it, I'm now getting this error--I'm not sure what to even tell my IT guy--can you help?:
Request failed with status code 401: oauth_problem=signature_invalid&oauth_signature=SvmFM214r%2BrlRskIOGWcuAu2ztVw50ck4V%2F6JYdSMNP0WcGiWy9BT9vm3mqGRlPHo0mhDwCaXMM7k4vy79J3ApEAw1BXcC%2FVUEePmSmGi4YY5L4Loh8SmgQtQU9wgOpvP%2FhebnJH7004Dqmhx%2FgfU2o57Grg4rwxJ%2FUGl3gxnDYhXOqn%2Fql8grq3yYEpnQjg2Ez6e1lyV7Sn0FRdmpp%2BA2&oauth_signature_base_string=GET%26https%253A%252F%252Fconfluence.bigxxxari-gc.com%252Frest%252Fconfanalytics%252F1.0%252Finstance%252Fpaginated%252FactivityBySpace%26content%253Dpage%2526fromDate%253D2026-07-24T13%25253A36%25253A35.550Z%2526limit%253D100%2526oauth_consumer_key%253DConfluence%25253A7714378250%2526oauth_nonce%253D5b67a4a4-5033-4ddf-b7fe-da28f1c3617e_9897774749961085%2526oauth_signature_method%253DRSA-SHA1%2526oauth_timestamp%253D1787578595%2526oauth_token%253D%2526oauth_version%253D1.0%2526period%253Dweek%2526sortField%253DVIEWED_COUNT%2526sortOrder%253DASC%2526spaceType%253Dglobal%2526timezone%253DGMT%25252B00%25253A00%2526toDate%253D2026-08-24T13%25253A36%25253A35.550Z%2526type%253Dtotal%2526xoauth_requestor_id%253Dkdickason%25252540xxaci.com&oauth_signature_method=RSA-SHA1

Evgenii
Community Champion
August 24, 2026

Very strange. 
I took code from vendor page, where they gave exactly this code, as example of new logic
https://docs.adaptavist.com/sr4c/9.x/release-notes/breaking-changes#api-changes
I slightly modified it, added some error catching, maybe that influenced.

Try this code, it's vendor code without my changes:

import com.atlassian.oauth.Request
import org.joda.time.DateTime
import groovy.json.JsonSlurper
import groovy.xml.MarkupBuilder
import org.joda.time.DateTimeZone
import groovyx.net.http.URIBuilder
import com.atlassian.sal.api.UrlMode
import com.atlassian.sal.api.ApplicationProperties
import com.onresolve.scriptrunner.parameters.annotation.Select
import com.onresolve.scriptrunner.parameters.annotation.meta.Option
import com.onresolve.scriptrunner.runner.customisers.PluginModule
import java.net.http.HttpClient
import java.net.http.HttpRequest
import java.net.http.HttpResponse

@PluginModule
ApplicationProperties applicationProperties

@Select(
label = "Space Type",
description = "Select the <b>'Space Type'</b> you wish to include in your result",
options = [
@Option(label = "Global", value = "global"),
@Option(label = "Personal", value = "personal"),
@Option(label = "Global & Personal", value = "global,personal"),
]
)
String spaceType

@Select(
label = "Content",
description = "Select the <b>'Content'</b> you wish to include in your result ",
options = [
@Option(label = "Page", value = "page"),
@Option(label = "Blog", value = "blog"),
@Option(label = "Page & Blog", value = "page,blog"),
]
)
String content

@Select(
label = "Sort Field",
description = "This is to set which column you want to sort as <b>'Last Viewed'</b> or <b>'Total Views'</b> in the result.",
options = [
@Option(label = "Last Viewed", value = "VIEWED_LAST_DATE"),
@Option(label = "Total Views", value = "VIEWED_COUNT"),
]
)
String sortField

@Select(
label = "Sort Order",
description = "This is to set which column you want to sort the <b>'Sort Field'</b> above to either ASC or DESC.",
options = [
@Option(label = "Ascending", value = "ASC"),
@Option(label = "Descending", value = "DESC"),
]
)
String sortOrder

// Number of days you want to minus from current date. This is one of parameter (Date Ranges) needed in the rest request.
def days = 100
def toDate = new DateTime( new DateTime() , DateTimeZone.UTC )
def fromDate = toDate.minusDays(days)

// Rest request to get the activityBySpace
def rest = '/rest/confanalytics/1.0/instance/paginated/activityBySpace'
def host = applicationProperties.getBaseUrl(UrlMode.CANONICAL)

def url = new URIBuilder( host )
.setPath(host + rest)
.addQueryParam("fromDate", fromDate)
.addQueryParam("toDate", toDate)
.addQueryParam("period", "week")
.addQueryParam("spaceType", spaceType)
.addQueryParam("content", content)
.addQueryParam("timezone", "GMT+00:00")
.addQueryParam("type", "total")
.addQueryParam("limit", "100")
.addQueryParam("sortField", sortField)
.addQueryParam("sortOrder", sortOrder) as String

try {
def request = HttpRequest.newBuilder()
.uri(OAuthRequestSigner.createOAuthUri(url))
.header("Content-Type", "application/json")
.header("Authorization", OAuthRequestSigner.createAuthorizationHeader(url, Request.HttpMethod.GET))
.GET()
.build()

def response = HttpClient.newHttpClient()
.send(request, HttpResponse.BodyHandlers.ofString())

if (response.statusCode() >= 400) {
throw new Exception("Status code: ${response.statusCode()}. Response body: ${response.body()}")
}

def result = new JsonSlurper().parseText(response.body()) as Map

def activityBySpace = result.activityBySpace as List<Map>

def stringWriter = new StringWriter()
def build = new MarkupBuilder(stringWriter)

// build the output into a table
build.table(class: "aui") {
tbody {
tr {
th { p("Space Key") }
th { p("Space Name") }
th { p("Last Viewed") }
th { p("Total Views") }
}
}
activityBySpace.each { space ->
def spaceLink = "<a href='${space?.link}'>${space?.name}</a>"
tr {
td { p(space?.key) }
td { mkp.yieldUnescaped(spaceLink) }
td { p(space?.lastViewedAt[0..9]) }
td { p(space?.views) }
}
}
}
stringWriter.toString()
} catch ( Exception e ) {
log.warn "Exception >>> ${e.message}"
return e.message
}



 

Kathy Dickason
Contributor
August 24, 2026

I'm still getting Status code: 401. Response body: oauth_problem=signature_invalid&oauth_signature....

I do really appreciate your help.  Here's what I need exactly:  The number of Views for each Global space on my Data Center Confluence 10.2.14 for the last 31 days.  I don't need any more customizations than that. 

Evgenii
Community Champion
August 24, 2026

I tried to google it, and it looks like, problem is not in script. Something wrong with instance configuration, maybe Confluence is behind reverse-proxy?
Your IT guy can check that the connector in Confluence's server.xml has scheme="https", secure="true", proxyName="confluence.bigxxxari-gc.com" and proxyPort="443", and that the proxy forwards the Host header unchanged.


Kathy Dickason
Contributor
August 24, 2026

Yes, me too. I've relayed what you wrote above.  I will post here and accept your answer as soon as I can.

 

Like • Evgenii likes this
Kathy Dickason
Contributor
August 24, 2026

The problem is our internal configuration.  Your first posted script (and the other) was clean and I'm sure it will work once we get our issue resolved. Thank you!!

 

Evgenii
Community Champion
August 24, 2026

Great! 
Good luck!

Suggest an answer

Log in or Sign up to answer
DEPLOYMENT TYPE
SERVER
TAGS
AUG Leaders

Atlassian Community Events