uaparserjs

July 9, 2026 · View on GitHub

License

uaparserjs

Parses HTTP user agent strings and returns user agent, device and OS information. This is a ‘V8’-backed package based on the ‘ua-parser’ project https://github.com/ua-parser.

Functions

The following functions are implemented:

  • ua_parse: Parse a vector of user agents into a data frame

Installation

install.packages("uaparserjs")

Example Usage

library(uaparserjs)

# current version
packageVersion("uaparserjs")
## [1] '0.4.0'
dplyr::glimpse(ua_parse("Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/535.2 (KHTML, like Gecko) Ubuntu/11.10 Chromium/15.0.874.106 Chrome/15.0.874.106 Safari/535.2"))
## Rows: 1
## Columns: 9
## $ userAgent     <chr> "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/535.2 (KHTM…
## $ ua.family     <chr> "Chromium"
## $ ua.major      <chr> "15"
## $ ua.minor      <chr> "0"
## $ ua.patch      <chr> "874"
## $ os.family     <chr> "Ubuntu"
## $ os.major      <chr> "11"
## $ os.minor      <chr> "10"
## $ device.family <chr> "Other"
agents <- readLines(system.file("extdat", "agents.txt", package = "uaparserjs"))

dplyr::glimpse(ua_parse(agents))
## Rows: 1,091
## Columns: 13
## $ userAgent     <chr> "Mozilla/5.0 (Windows; U; en-US) AppleWebKit/531.9 (KHTM…
## $ ua.family     <chr> "AdobeAIR", "Amazon Silk", "Amazon Silk", "Amazon Silk",…
## $ ua.major      <chr> "2", "1", "2", "2", "2", "3", "2", "2", "2", "2", "3", "…
## $ ua.minor      <chr> "5", "1", "0", "1", "2", "25", "2", "3", "3", "3", "0", …
## $ ua.patch      <chr> "1", "0-80", NA, NA, NA, NA, "2", "3", "4", "5", "1", "3…
## $ os.family     <chr> "Windows", "Android", "Android", "Android", "Android", "…
## $ device.family <chr> "Other", "Kindle", "Kindle Fire HD", "Kindle Fire", "Kin…
## $ device.brand  <chr> NA, "Amazon", "Amazon", "Amazon", "Amazon", "Amazon", "H…
## $ device.model  <chr> NA, "Kindle", "Kindle Fire HD 7\"", "Kindle Fire", "Kind…
## $ os.major      <chr> NA, NA, NA, NA, NA, "4", "2", "2", "2", "2", "3", "4", "…
## $ os.minor      <chr> NA, NA, NA, NA, NA, "0", "2", "3", "3", "3", "0", "0", "…
## $ os.patch      <chr> NA, NA, NA, NA, NA, "3", "2", "3", "4", "5", "1", "3", "…
## $ os.patchMinor <chr> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, …

Performance

Time for parsing small numbers of UA strings with useNA defaulting to FALSE and list cache

set.seed(100)
#batch_100 <- sample(agents, 100)
assign("cache", list(), envir=uaparserjs:::.pkgenv)
microbenchmark::microbenchmark(
  ua_parse(agents)
)
## Unit: seconds
##              expr      min       lq     mean   median       uq      max neval
##  ua_parse(agents) 1.230242 1.274419 1.306185 1.299452 1.332416 1.522362   100

Time for parsing small numbers of UA strings with list cache and useNA=TRUE

set.seed(100)
#batch_100 <- sample(agents, 100)
assign("cache", list(), envir=uaparserjs:::.pkgenv)
microbenchmark::microbenchmark(
  ua_parse(agents,useNA=TRUE)
)
## Unit: milliseconds
##                            expr      min      lq     mean   median       uq
##  ua_parse(agents, useNA = TRUE) 31.55939 36.4024 44.03564 38.06212 39.90679
##       max neval
##  582.4312   100

Time for parsing small numbers of UA strings with environment cache and useNA=TRUE

set.seed(100)
#batch_100 <- sample(agents, 100)
assign("cache", new.env(), envir=uaparserjs:::.pkgenv)
microbenchmark::microbenchmark(
  ua_parse(agents,useNA=TRUE)
)
## Unit: milliseconds
##                            expr      min       lq     mean   median       uq
##  ua_parse(agents, useNA = TRUE) 27.53631 33.13031 40.71838 35.76616 37.97613
##       max neval
##  535.2129   100

Effect of useNA

This addresses relative performance with and without the useNA flag. This test exercises the plumbing that handles unpacking the parameter list and compilation of the results with effectively no cache involvement. This test minimises the effect of caching by using the same UA string over and over. This test does not call the underlying Javascript parser repeatedly, values are passed to the JavaScript parser one at a time and parse results are cached for each user agent.

Raw data

class(results2) = "data.frame"
results2
##                                                                                     expr
## 1                                     uaparserjs::ua_parse(rep(workingUA, i), useNA = f)
## 2          uaparserjs::ua_parse(rep(workingUA, i), useNA = f).list(1, 2048688, 50, TRUE)
## 3         uaparserjs::ua_parse(rep(workingUA, i), useNA = f).list(1, 3093938, 100, TRUE)
## 4        uaparserjs::ua_parse(rep(workingUA, i), useNA = f).list(1, 11911549, 500, TRUE)
## 5       uaparserjs::ua_parse(rep(workingUA, i), useNA = f).list(1, 23629774, 1000, TRUE)
## 6      uaparserjs::ua_parse(rep(workingUA, i), useNA = f).list(1, 131288542, 5000, TRUE)
## 7     uaparserjs::ua_parse(rep(workingUA, i), useNA = f).list(1, 319359199, 10000, TRUE)
## 8    uaparserjs::ua_parse(rep(workingUA, i), useNA = f).list(1, 1448103817, 50000, TRUE)
## 9        uaparserjs::ua_parse(rep(workingUA, i), useNA = f).list(1, 14191446, 10, FALSE)
## 10       uaparserjs::ua_parse(rep(workingUA, i), useNA = f).list(1, 54778869, 50, FALSE)
## 11     uaparserjs::ua_parse(rep(workingUA, i), useNA = f).list(1, 111332221, 100, FALSE)
## 12     uaparserjs::ua_parse(rep(workingUA, i), useNA = f).list(1, 538215923, 500, FALSE)
## 13   uaparserjs::ua_parse(rep(workingUA, i), useNA = f).list(1, 1077447407, 1000, FALSE)
## 14   uaparserjs::ua_parse(rep(workingUA, i), useNA = f).list(1, 5369082177, 5000, FALSE)
## 15 uaparserjs::ua_parse(rep(workingUA, i), useNA = f).list(1, 10456872749, 10000, FALSE)
## 16 uaparserjs::ua_parse(rep(workingUA, i), useNA = f).list(1, 53705485643, 50000, FALSE)
##           time UAcount useNA      timesec
## 1      2277664      10  TRUE  0.002277664
## 2      2048688      50  TRUE  0.002048688
## 3      3093938     100  TRUE  0.003093938
## 4     11911549     500  TRUE  0.011911549
## 5     23629774    1000  TRUE  0.023629774
## 6    131288542    5000  TRUE  0.131288542
## 7    319359199   10000  TRUE  0.319359199
## 8   1448103817   50000  TRUE  1.448103817
## 9     14191446      10 FALSE  0.014191446
## 10    54778869      50 FALSE  0.054778869
## 11   111332221     100 FALSE  0.111332221
## 12   538215923     500 FALSE  0.538215923
## 13  1077447407    1000 FALSE  1.077447407
## 14  5369082177    5000 FALSE  5.369082177
## 15 10456872749   10000 FALSE 10.456872749
## 16 53705485643   50000 FALSE 53.705485643

Effect of cache misses with large cache sizes

This examines the worst-case effect of cache misses with large cache sizes. The cache is populated with a number of unique strings, the number matching the test case size.
Every measured call experiences that number of cache misses before finding its value in the cache, for example with 1000 User Agent strings passed in 1000 unique cache entries are created before the test and the test finds the last cache entry.

The test is coded to force use of a list() because list will be removed in the next release (see test results below).

results = c()

for(f in naFlags)
{
  for(i in counts)
  {
    unloadNamespace("uaparserjs")
    assign("cache", list(), envir=uaparserjs:::.pkgenv)

    for(j in 1:i)
    {
      # fake the right number of UA strings for this request cycle
      uaparserjs::ua_parse(paste(j, workingUA),useNA = TRUE)
    }
    bmResult = microbenchmark::microbenchmark(uaparserjs::ua_parse(rep(workingUA,i),useNA = f),times = 1)
    bmResult$UAcount = i
    bmResult$useNA = f
    results = c(results, list(bmResult))
  }  
}

results2 = do.call("rbind", results)

results2$timesec = results2$time/1000000000

library(ggplot2) 

ggplot(results2, aes(UAcount)) + 
 aes(x=UAcount, y=timesec, colour=useNA) + 
 geom_line() +
 labs(
    title = "Elapsed Time for varying numbers of user agent strings per request",
    subtitle = "100% cache misses, cache size = request size",
    x = "Number of User Agent Strings",
    y = "Elased Time (seconds)",
    color = "useNA parameter values"
  )

Raw Data

class(results2) = "data.frame"
results2
##                                                                                      expr
## 1                                      uaparserjs::ua_parse(rep(workingUA, i), useNA = f)
## 2           uaparserjs::ua_parse(rep(workingUA, i), useNA = f).list(1, 3330069, 50, TRUE)
## 3          uaparserjs::ua_parse(rep(workingUA, i), useNA = f).list(1, 4165148, 100, TRUE)
## 4         uaparserjs::ua_parse(rep(workingUA, i), useNA = f).list(1, 17082272, 500, TRUE)
## 5        uaparserjs::ua_parse(rep(workingUA, i), useNA = f).list(1, 32123521, 1000, TRUE)
## 6       uaparserjs::ua_parse(rep(workingUA, i), useNA = f).list(1, 276723239, 5000, TRUE)
## 7     uaparserjs::ua_parse(rep(workingUA, i), useNA = f).list(1, 1011251300, 10000, TRUE)
## 8    uaparserjs::ua_parse(rep(workingUA, i), useNA = f).list(1, 62306937133, 50000, TRUE)
## 9         uaparserjs::ua_parse(rep(workingUA, i), useNA = f).list(1, 12241732, 10, FALSE)
## 10        uaparserjs::ua_parse(rep(workingUA, i), useNA = f).list(1, 51719883, 50, FALSE)
## 11      uaparserjs::ua_parse(rep(workingUA, i), useNA = f).list(1, 105408103, 100, FALSE)
## 12      uaparserjs::ua_parse(rep(workingUA, i), useNA = f).list(1, 520955656, 500, FALSE)
## 13    uaparserjs::ua_parse(rep(workingUA, i), useNA = f).list(1, 1115157118, 1000, FALSE)
## 14    uaparserjs::ua_parse(rep(workingUA, i), useNA = f).list(1, 7709131441, 5000, FALSE)
## 15  uaparserjs::ua_parse(rep(workingUA, i), useNA = f).list(1, 21918423943, 10000, FALSE)
## 16 uaparserjs::ua_parse(rep(workingUA, i), useNA = f).list(1, 434529588060, 50000, FALSE)
##            time UAcount useNA       timesec
## 1       2221922      10  TRUE   0.002221922
## 2       3330069      50  TRUE   0.003330069
## 3       4165148     100  TRUE   0.004165148
## 4      17082272     500  TRUE   0.017082272
## 5      32123521    1000  TRUE   0.032123521
## 6     276723239    5000  TRUE   0.276723239
## 7    1011251300   10000  TRUE   1.011251300
## 8   62306937133   50000  TRUE  62.306937133
## 9      12241732      10 FALSE   0.012241732
## 10     51719883      50 FALSE   0.051719883
## 11    105408103     100 FALSE   0.105408103
## 12    520955656     500 FALSE   0.520955656
## 13   1115157118    1000 FALSE   1.115157118
## 14   7709131441    5000 FALSE   7.709131441
## 15  21918423943   10000 FALSE  21.918423943
## 16 434529588060   50000 FALSE 434.529588060

Effect of cache misses with large cache sizes using an environment instead of list as cache.

Using an environment, the only native hashmap structure in base R, worst case performance is significantly better than using a list. How much this matters depends very much on the distribution of UAs in the input data, but when dealing with two to four million access log records its worth recovering every possible second.

results = c()

library(uaparserjs)

for(f in naFlags)
{
  for(i in counts)
  {
    assign("cache", new.env(), envir=uaparserjs:::.pkgenv)
    for(j in 1:i)
    {
      # fake the right number of UA strings for this request cycle
      uaparserjs::ua_parse(paste(j, workingUA),useNA = TRUE)
    }
    bmResult = microbenchmark::microbenchmark(uaparserjs::ua_parse(rep(workingUA,i),useNA = f),times = 1)
    bmResult$UAcount = i
    bmResult$useNA = f
    results = c(results, list(bmResult))
  }  
}

results3 = do.call("rbind", results)

results3$timesec = results3$time/1000000000

library(ggplot2) 

ggplot(results3, aes(UAcount)) + 
 aes(x=UAcount, y=timesec, colour=useNA) + 
 geom_line() +
 labs(
    title = "Elapsed Time for varying numbers of user agent strings per request",
    subtitle = "100% cache misses, cache size = request size",
    x = "Number of User Agent Strings",
    y = "Elased Time (seconds)",
    color = "useNA parameter values"
  )

Raw Data

class(results3) = "data.frame"
results3
##                                                                                     expr
## 1                                     uaparserjs::ua_parse(rep(workingUA, i), useNA = f)
## 2          uaparserjs::ua_parse(rep(workingUA, i), useNA = f).list(1, 3612107, 50, TRUE)
## 3         uaparserjs::ua_parse(rep(workingUA, i), useNA = f).list(1, 4212055, 100, TRUE)
## 4        uaparserjs::ua_parse(rep(workingUA, i), useNA = f).list(1, 13765687, 500, TRUE)
## 5       uaparserjs::ua_parse(rep(workingUA, i), useNA = f).list(1, 25481705, 1000, TRUE)
## 6      uaparserjs::ua_parse(rep(workingUA, i), useNA = f).list(1, 128063436, 5000, TRUE)
## 7     uaparserjs::ua_parse(rep(workingUA, i), useNA = f).list(1, 258059169, 10000, TRUE)
## 8    uaparserjs::ua_parse(rep(workingUA, i), useNA = f).list(1, 1394473442, 50000, TRUE)
## 9        uaparserjs::ua_parse(rep(workingUA, i), useNA = f).list(1, 16555736, 10, FALSE)
## 10       uaparserjs::ua_parse(rep(workingUA, i), useNA = f).list(1, 53977570, 50, FALSE)
## 11     uaparserjs::ua_parse(rep(workingUA, i), useNA = f).list(1, 102167137, 100, FALSE)
## 12     uaparserjs::ua_parse(rep(workingUA, i), useNA = f).list(1, 532422069, 500, FALSE)
## 13   uaparserjs::ua_parse(rep(workingUA, i), useNA = f).list(1, 1062102735, 1000, FALSE)
## 14   uaparserjs::ua_parse(rep(workingUA, i), useNA = f).list(1, 5524649017, 5000, FALSE)
## 15 uaparserjs::ua_parse(rep(workingUA, i), useNA = f).list(1, 10968916653, 10000, FALSE)
## 16 uaparserjs::ua_parse(rep(workingUA, i), useNA = f).list(1, 52765686701, 50000, FALSE)
##           time UAcount useNA      timesec
## 1      3829436      10  TRUE  0.003829436
## 2      3612107      50  TRUE  0.003612107
## 3      4212055     100  TRUE  0.004212055
## 4     13765687     500  TRUE  0.013765687
## 5     25481705    1000  TRUE  0.025481705
## 6    128063436    5000  TRUE  0.128063436
## 7    258059169   10000  TRUE  0.258059169
## 8   1394473442   50000  TRUE  1.394473442
## 9     16555736      10 FALSE  0.016555736
## 10    53977570      50 FALSE  0.053977570
## 11   102167137     100 FALSE  0.102167137
## 12   532422069     500 FALSE  0.532422069
## 13  1062102735    1000 FALSE  1.062102735
## 14  5524649017    5000 FALSE  5.524649017
## 15 10968916653   10000 FALSE 10.968916653
## 16 52765686701   50000 FALSE 52.765686701