組み込みコネクタの欠点を回避するために、Analytics API に直接接続するという同様のニーズがありました。Web バージョンの PowerBI が認証エンドポイントを「匿名」ソースとして受け入れるようにするのは少し厄介でしたが、リバース プロキシは「プローブ」GET 要求に200 OK
. 主な PowerQuery / M ロジックを関数に分割したものを次に示します。
GetAccessToken_GA
let
Source = (optional nonce as text) as text => let
// use `nonce` to force a fresh fetch
someNonce = if nonce = null or nonce = ""
then "nonce"
else nonce,
// Reverse proxy required to trick PowerBI Cloud into allowing its malformed "anonymous" requests to return `200 OK`.
// We can skip this and connect directly to GA, but then the Web version will not be able to refresh.
url = "https://obfuscated.herokuapp.com/oauth2/v4/token",
GetJson = Web.Contents(url,
[
Headers = [
#"Content-Type"="application/x-www-form-urlencoded"
],
Content = Text.ToBinary(
// "code=" & #"Google API - Auth Code"
// "&redirect_uri=urn:ietf:wg:oauth:2.0:oob"
"refresh_token=" & #"Google API - Refresh Token"
& "&client_id=" & #"Google API - Client ID"
& "&client_secret=" & #"Google API - Client Secret"
// & "&scope=https://www.googleapis.com/auth/analytics.readonly"
& "&grant_type=refresh_token"
& "&nonce=" & someNonce
)
]
),
FormatAsJson = Json.Document(GetJson),
// Gets token from the Json response
AccessToken = FormatAsJson[access_token],
AccessTokenHeader = "Bearer " & AccessToken
in
AccessTokenHeader
in
Source
returnAccessHeaders_GA
nonce は GA API では使用されません。ここでは、Power BI が最大 1 分間 API 要求をキャッシュできるようにするために使用しました。
let
returnAccessHeaders = () as text => let
nonce = DateTime.ToText(DateTime.LocalNow(), "yyyyMMddhhmm"),
AccessTokenHeader = GetAccessToken_GA(nonce)
in
AccessTokenHeader
in
returnAccessHeaders
parseJsonResponse_GA
let
fetcher = (jsonResponse as binary) as table => let
FormatAsJsonQuery = Json.Document(jsonResponse),
columnHeadersGA = FormatAsJsonQuery[columnHeaders],
listRows = Record.FieldOrDefault(
FormatAsJsonQuery,
"rows",
{List.Transform(columnHeadersGA, each null)}
// a list of (lists of length exactly matching the # of columns) of null
),
columnNames = List.Transform(columnHeadersGA, each Record.Field(_, "name")),
matchTypes = (column as record) as list => let
values = {
{ "STRING", type text },
{ "FLOAT", type number },
{ "INTEGER", Int64.Type },
{ "TIME", type number },
{ "PERCENT", type number },
{ column[dataType], type text } // default type
},
columnType = List.First(
List.Select(
values,
each _{0} = column[dataType]
)
){1},
namedColumnType = { column[name], columnType }
in namedColumnType,
recordRows = List.Transform(
listRows,
each Record.FromList(_, columnNames)
),
columnTypes = List.Transform(columnHeadersGA, each matchTypes(_)),
rowsTable = Table.FromRecords(recordRows),
typedRowsTable = Table.TransformColumnTypes(rowsTable, columnTypes)
in typedRowsTable
in fetcher
fetchAndParseGA
への最初のパラメーターWeb.Contents()
は文字列リテラルでなければなりません。
let
AccessTokenHeader = returnAccessHeaders_GA(),
fetchAndParseGA_fn = (url as text) as table => let
JsonQuery = Web.Contents(
"https://gapis-powerbi-revproxy.herokuapp.com/analytics",
[
RelativePath = url,
Headers = [
#"Authorization" = AccessTokenHeader
]
]
),
Response = parseJsonResponse_GA(JsonQuery)
in
Response
in
fetchAndParseGA_fn
queryUrlHelper
自動 URL エンコーディングを使用して、Power BI の「ステップ エディター」UI を使用してクエリ パラメーターを調整できるようにします。
let
safeString = (s as nullable text) as text => let
result = if s = null
then ""
else s
in
result,
uriEncode = (s as nullable text) as text => let
result = Uri.EscapeDataString(safeString(s))
in
result,
optionalParam = (name as text, s as nullable text) => let
result = if s = null or s = ""
then ""
else "&" & name & "=" & uriEncode(s)
in
result,
queryUrlHelper = (
gaID as text,
startDate as text,
endDate as text,
metrics as text,
dimensions as nullable text,
sort as nullable text,
filters as nullable text,
segment as nullable text,
otherParameters as nullable text
) as text => let
result = "/v3/data/ga?ids=" & uriEncode(gaID)
& "&start-date=" & uriEncode(startDate)
& "&end-date=" & uriEncode(endDate)
& "&metrics=" & uriEncode(metrics)
& optionalParam("dimensions", dimensions)
& optionalParam("sort", sort)
& optionalParam("filters", filters)
& optionalParam("segment", segment)
& safeString(otherParameters)
in
result,
Example = queryUrlHelper(
"ga:59361446", // gaID
"MONTHSTART", // startDate
"MONTHEND", // endDate
"ga:sessions,ga:pageviews", // metrics
"ga:userGender", // dimensions
"-ga:sessions", // sort
null, // filters
"gaid::BD_Im9YKTJeO9xDxV4w6Kw", // segment
null // otherParameters (must be manually url-encoded, and start with "&")
)
in
queryUrlHelper
getLinkForQueryExplorer
Query Explorerでクエリを開くのに便利です。
let
getLinkForQueryExplorer = (querySuffixUrl as text) as text => let
// querySuffixUrl should start like `/v3/data/ga?ids=ga:132248814&...`
link = Text.Replace(
querySuffixUrl,
"/v3/data/ga",
"https://ga-dev-tools.appspot.com/query-explorer/"
)
in
link
in
getLinkForQueryExplorer
Identity
入力を変更せずに返します。この関数の主な用途は、便利な「ステップ エディター」UI を介して別の方法でクエリ変数を更新できるようにすることです。
let
Identity = (x as any) as any => let
x = x
in
x
in
Identity
getMonthBoundary
// Get a list of the start and end dates of the relative month, as ISO 8601 formatted dates.
//
// The end date of the current month is considered to be the current date.
//
// E.g.:
// ```
// {
// "2016-09-01",
// "2016-09-31"
// }
// ```
//
// Source: <https://gist.github.com/r-k-b/db1eb0e00364cb592e1d8674bb03cb5c>
let
GetMonthDates = (monthOffset as number) as list => let
now = DateTime.LocalNow(),
otherMonth = Date.AddMonths(now, monthOffset),
month1Start = Date.StartOfMonth(otherMonth),
month1End = Date.AddDays(Date.EndOfMonth(otherMonth), -1),
dates = {
month1Start,
month1End
},
result = List.Transform(
dates,
each DateTime.ToText(_, "yyyy-MM-dd")
)
in
result
in
GetMonthDates
replaceUrlDates
//
// E.g., on 2016-10-19 this is the result:
// ```
// replaceDates(-1, "/foo?s=MONTHSTART&e=MONTHEND") === "/foo?s=2016-09-01&e=2016-09-28"
// ```
let
replaceDates = (monthOffset as number, rawUrl as text) as text => let
boundaryList = getMonthBoundary(monthOffset),
stage01 = Text.Replace(
rawUrl,
"MONTHSTART",
boundaryList{0}
),
stage02 = Text.Replace(
stage01,
"MONTHEND",
boundaryList{1}
),
stage03 = replaceViewNames(stage02)
in
stage03
in
replaceDates
クエリの例
let
QueryBase = queryUrlHelper("All Web Site Data", "MONTHSTART", "today", "ga:sessions,ga:pageviews,ga:pageviewsPerSession", "ga:deviceCategory,ga:yearMonth", null, null, null, null),
MonthOffset = Identity(#"Months Back to Query"),
QueryURL = replaceUrlDates(MonthOffset, QueryBase),
CopyableLinkToQueryExplorer = getLinkForQueryExplorer(QueryURL),
Source = fetchAndParseGA(QueryURL)
in
Source
おまけとして、これは任意の OAuthV2 データ ソースに一般化でき、強力な V4 API を操作するための調整も最小限で済みます。