diff --git a/Project.toml b/Project.toml index ab03901..e3de64e 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "mPulseAPI" uuid = "314d2b54-f2c3-11ea-15f2-0bcf2fc50b35" authors = ["Akamai mPulse DSWB "] -version = "1.1.4" +version = "1.2.0" [deps] DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0" diff --git a/docs/src/BeaconAPI.md b/docs/src/BeaconAPI.md new file mode 100644 index 0000000..79a6696 --- /dev/null +++ b/docs/src/BeaconAPI.md @@ -0,0 +1,13 @@ +# Sending Beacons + +mPulseAPI.jl uses the mPulse [REST Beacon API](https://techdocs.akamai.com/mpulse/reference/beacons#rest-api) to send beacons. + +You first need to make a config request using [`mPulseAPI.getBeaconConfig`](@ref) and then include that config in your call to +[`mPulseAPI.sendBeacon`](@ref) along with other beacon parameters. + +Beacon parameters may be named by label or name. + +```@autodocs +Modules = [mPulseAPI] +Pages = ["BeaconAPI.jl"] +``` diff --git a/src/BeaconAPI.jl b/src/BeaconAPI.jl new file mode 100644 index 0000000..46c22b2 --- /dev/null +++ b/src/BeaconAPI.jl @@ -0,0 +1,104 @@ +""" +Base URL for config.json requests +""" +const CONFIG_URL = "https://c.go-mpulse.net/api/config.json" + +""" +Fetch beacon configuration for an mPulse APP using the [Beacon API](https://techdocs.akamai.com/mpulse/reference/beacons#rest-api) +Caches the result for whatever is specified in `Cache-control: max-age` +""" +function getBeaconConfig(appKey::AbstractString, appDomain::AbstractString) + config_obj = getObjectFromCache("beacon-config", Dict(:appKey => appKey)) + + if !isnothing(config_obj) + return config_obj + end + + config = HTTP.get(CONFIG_URL, query = Dict("key" => appKey, "d" => appDomain)) + + cache_headers = filter(h -> lowercase(h[1]) == "cache-control", config.headers) + if isempty(cache_headers) + cache_headers = Dict{AbstractString, AbstractString}() + else + cache_headers = Dict{AbstractString, AbstractString}( + map( + x -> Pair([split(x, "="); ""][1:2]...), + split( + mapreduce( + h -> h[2], + (l, r) -> string(l, ", ", r), + cache_headers + ), + r", *" + ) + ) + ) + end + + expiry = Dates.Second(parse(Int, get(cache_headers, "max-age", "300"); base=10)) + + config_obj = JSON.parse(IOBuffer(config.body)) + + writeObjectToCache("beacon-config", Dict(:appKey => appKey), config_obj; expiry) + + return config_obj +end + +""" +Send a beacon to mPulse +""" +function sendBeacon(config::Dict, params::Dict) + beacon_url = "https:" * config["beacon_url"] + + now_ms = Int(datetime2unix(now())*1000) + + beacon_params = Dict{String, Any}( + "api" => 1, + "api.v" => 1, + "h.cr" => config["h.cr"], + "h.d" => config["h.d"], + "h.key" => config["h.key"], + "h.t" => config["h.t"], + "rt.end" => now_ms, + "rt.si" => get(params, "SessionID", config["session_id"]), + "rt.sl" => get(params, "SessionLength", 1), + "rt.ss" => get(params, "SessionStart", now_ms), + "rt.start" => "manual", + ) + + if haskey(params, "PageGroup") + beacon_params["h.pg"] = params["PageGroup"] + end + if haskey(params, "Url") + beacon_params["u"] = params["Url"] + end + if haskey(params, "tDone") + beacon_params["t_done"] = params["tDone"] + end + + vars = ["customMetrics", "customDimensions", "customTimers"] + t_other = [] + for v in vars + for p in config["PageParams"][v] + for k in ["label", "name"] + if haskey(params, p[k]) + if v == "customTimers" + push!(t_other, string(p["label"], "|", params[p[k]])) + else + beacon_params[p["label"]] = params[p[k]] + end + break + end + end + end + end + + if !isempty(t_other) + beacon_params["t_other"] = join(t_other, ",") + end + + + res = HTTP.get(beacon_url, query = beacon_params) + + return res.status == 204 +end diff --git a/src/cache_utilities.jl b/src/cache_utilities.jl index 480ffb8..b92149d 100644 --- a/src/cache_utilities.jl +++ b/src/cache_utilities.jl @@ -1,13 +1,10 @@ const caches = Dict{AbstractString, Dict}() -function writeObjectToCache(cacheType::AbstractString, searchKey::Dict{Symbol, Any}, object::Dict) +function writeObjectToCache(cacheType::AbstractString, searchKey::Dict{Symbol, <:Any}, object::Dict; expiry::TimePeriod=Dates.Hour(1)) if !haskey(caches, cacheType) caches[cacheType] = Dict() end - # Store object in cache for 1 hour - object["lastCached"] = now() - for ky in keys(searchKey) k = string(ky) v = searchKey[ky] @@ -16,12 +13,13 @@ function writeObjectToCache(cacheType::AbstractString, searchKey::Dict{Symbol, A end if v != nothing - caches[cacheType]["$(k)_$(v)"] = object + # Store object in cache for 1 hour + caches[cacheType]["$(k)_$(v)"] = (object = object, expiry = now() + expiry) end end end -function getObjectFromCache(cacheType::AbstractString, searchKey::Dict{Symbol, Any}, fetchStale::Bool=false) +function getObjectFromCache(cacheType::AbstractString, searchKey::Dict{Symbol, <:Any}, fetchStale::Bool=false) if !haskey(caches, cacheType) return nothing end @@ -30,8 +28,8 @@ function getObjectFromCache(cacheType::AbstractString, searchKey::Dict{Symbol, A for (k, v) in searchKey if isa(v, Number) ? v > 0 : v != "" - if haskey(cache, "$(k)_$(v)") && (fetchStale || cache["$(k)_$(v)"]["lastCached"] > now() - Dates.Hour(1)) - return cache["$(k)_$(v)"] + if haskey(cache, "$(k)_$(v)") && (fetchStale || cache["$(k)_$(v)"].expiry > now()) + return cache["$(k)_$(v)"].object end end end @@ -41,7 +39,7 @@ end # Internal convenience method for clearing object cache -function clearObjectCache(cacheType::AbstractString, searchKey::Dict{Symbol, Any}) +function clearObjectCache(cacheType::AbstractString, searchKey::Dict{Symbol, <:Any}) local object = getObjectFromCache(cacheType, searchKey) if object == nothing @@ -133,7 +131,9 @@ function clearTokenCache(tenant::AbstractString) # in previously to make authentication easier tenant = "tenant_$tenant" if haskey(caches["token"], tenant) - caches["token"][tenant]["lastCached"] = caches["token"][tenant]["tokenTimestamp"] = Date(0) + object = caches["token"][tenant].object + object["tokenTimestamp"] = Date(0) + caches["token"][tenant] = (expiry = Date(0), object = object) return true end diff --git a/src/mPulseAPI.jl b/src/mPulseAPI.jl index dcdfa97..8150b56 100644 --- a/src/mPulseAPI.jl +++ b/src/mPulseAPI.jl @@ -150,4 +150,6 @@ include(joinpath(@__DIR__, "Tenant.jl")) include(joinpath(@__DIR__, "Token.jl")) include(joinpath(@__DIR__, "QueryAPI.jl")) +include(joinpath(@__DIR__, "BeaconAPI.jl")) + end diff --git a/test/beacon-api.jl b/test/beacon-api.jl new file mode 100644 index 0000000..5ce8da5 --- /dev/null +++ b/test/beacon-api.jl @@ -0,0 +1,19 @@ +config = mPulseAPI.getBeaconConfig(beaconKey, "mPulseAPIDemo.net") + +@test config isa Dict +@test haskey(config, "site_domain") +@test config["site_domain"] == "mPulseAPIDemo.net" + +if !haskey(config, "rate_limited") + @test haskey(config, "h.cr") + @test haskey(config, "h.d") + @test haskey(config, "h.key") + @test haskey(config, "h.t") + + t_end = Int(datetime2unix(now())*1000) + @test mPulseAPI.sendBeacon(config, Dict("PageGroup" => "mPulseAPI Test", "tDone" => t_end - t_start, "Conversion" => 1, "ResourceTimer" => 500, "Url" => "https://github.com/akamai/mPulseAPI.jl/")) +end + +config2 = mPulseAPI.getBeaconConfig(beaconKey, "mPulseAPIDemo.net") + +@test config == config2 diff --git a/test/repository-tests.jl b/test/repository-tests.jl index 105c323..4562ef4 100644 --- a/test/repository-tests.jl +++ b/test/repository-tests.jl @@ -18,7 +18,7 @@ domainNames = map(d -> d["name"], domains) @test "mPulseAPI Test" ∈ domainNames # Get a specific domain -appKey = filter(d -> d["name"] == "mPulseAPI Test", domains)[1]["attributes"]["appKey"] +beaconKey = appKey = filter(d -> d["name"] == "mPulseAPI Test", domains)[1]["attributes"]["appKey"] @test !isempty(appKey) domain = getRepositoryDomain(token, appKey=appKey) diff --git a/test/runtests.jl b/test/runtests.jl index 09dc732..0b1f16a 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,5 +1,5 @@ using mPulseAPI -using Test +using Test, Dates # Check environment if !haskey(ENV, "mPulseAPIToken") @@ -28,6 +28,7 @@ end mPulseAPI.setVerbose(verbosity) +t_start = Int(datetime2unix(now())*1000) @testset "mPulseAPI" begin @testset "Repository" begin @@ -42,6 +43,10 @@ mPulseAPI.setVerbose(verbosity) include("query-tests.jl") end + @testset "Beacons" begin + include("beacon-api.jl") + end + @testset "Change URL" begin include("zzz_change-url-tests.jl") end