Skip to content

Commit

Permalink
Merge pull request #21 from akamai/beaconapi
Browse files Browse the repository at this point in the history
Add support for beaconing
  • Loading branch information
bluesmoon authored Oct 19, 2023
2 parents 68851a9 + 31354f5 commit 571d927
Show file tree
Hide file tree
Showing 8 changed files with 156 additions and 13 deletions.
2 changes: 1 addition & 1 deletion Project.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name = "mPulseAPI"
uuid = "314d2b54-f2c3-11ea-15f2-0bcf2fc50b35"
authors = ["Akamai mPulse DSWB <[email protected]>"]
version = "1.1.4"
version = "1.2.0"

[deps]
DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0"
Expand Down
13 changes: 13 additions & 0 deletions docs/src/BeaconAPI.md
Original file line number Diff line number Diff line change
@@ -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"]
```
104 changes: 104 additions & 0 deletions src/BeaconAPI.jl
Original file line number Diff line number Diff line change
@@ -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
20 changes: 10 additions & 10 deletions src/cache_utilities.jl
Original file line number Diff line number Diff line change
@@ -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]
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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

Expand Down
2 changes: 2 additions & 0 deletions src/mPulseAPI.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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
19 changes: 19 additions & 0 deletions test/beacon-api.jl
Original file line number Diff line number Diff line change
@@ -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
2 changes: 1 addition & 1 deletion test/repository-tests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
7 changes: 6 additions & 1 deletion test/runtests.jl
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
using mPulseAPI
using Test
using Test, Dates

# Check environment
if !haskey(ENV, "mPulseAPIToken")
Expand Down Expand Up @@ -28,6 +28,7 @@ end

mPulseAPI.setVerbose(verbosity)

t_start = Int(datetime2unix(now())*1000)

@testset "mPulseAPI" begin
@testset "Repository" begin
Expand All @@ -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
Expand Down

2 comments on commit 571d927

@bluesmoon
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@JuliaRegistrator register()

@JuliaRegistrator
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Registration pull request created: JuliaRegistries/General/93728

After the above pull request is merged, it is recommended that a tag is created on this repository for the registered package version.

This will be done automatically if the Julia TagBot GitHub Action is installed, or can be done manually through the github interface, or via:

git tag -a v1.2.0 -m "<description of version>" 571d9278dc24e82247fd6e3e76b0bb16de98dfe2
git push origin v1.2.0

Please sign in to comment.