Skip to content

Commit

Permalink
Merge pull request #92 from fastly/joeshaw/device-detection
Browse files Browse the repository at this point in the history
  • Loading branch information
joeshaw authored Nov 16, 2023
2 parents 33b3144 + 69551eb commit bc56fc2
Show file tree
Hide file tree
Showing 9 changed files with 357 additions and 1 deletion.
2 changes: 1 addition & 1 deletion .github/workflows/integration-tests.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name: Integration Tests
on: [push]
env:
VICEROY_VERSION: 0.9.2
VICEROY_VERSION: 0.9.3
jobs:
integration-tests-tinygo:
strategy:
Expand Down
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
## Unreleased

### Added

- Add support for device detection (`device`)

## 1.1.0 (2023-10-31)

### Added
Expand Down
9 changes: 9 additions & 0 deletions _examples/device-detection/fastly.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# This file describes a Fastly Compute package. To learn more visit:
# https://developer.fastly.com/reference/fastly-toml/

authors = ["oss@fastly.com"]
description = ""
language = "go"
manifest_version = 2
name = "device-detection"
service_id = ""
23 changes: 23 additions & 0 deletions _examples/device-detection/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// Copyright 2022 Fastly, Inc.

package main

import (
"context"
"fmt"

"github.com/fastly/compute-sdk-go/device"
"github.com/fastly/compute-sdk-go/fsthttp"
)

func main() {
fsthttp.ServeFunc(func(ctx context.Context, w fsthttp.ResponseWriter, r *fsthttp.Request) {
d, err := device.Lookup(r.Header.Get("User-Agent"))
if err != nil {
fsthttp.Error(w, err.Error(), fsthttp.StatusInternalServerError)
return
}

fmt.Fprintf(w, "%+v\n", d)
})
}
142 changes: 142 additions & 0 deletions device/device_detection.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
// Package device provides device dection based on the User-Agent
// header.
package device

import (
"encoding/json"
"errors"
"fmt"

"github.com/fastly/compute-sdk-go/internal/abi/fastly"
)

var (
// ErrDeviceNotFound is returned when the device is not found.
ErrDeviceNotFound = errors.New("device not found")

// ErrUnexpected indicates that an unexpected error occurred.
ErrUnexpected = errors.New("unexpected error")
)

type Device struct {
info deviceInfo
}

type deviceInfo struct {
Device struct {
Name string `json:"name"`
Brand string `json:"brand"`
Model string `json:"model"`
HWType string `json:"hwtype"`
IsEReader bool `json:"is_ereader"`
IsGameConsole bool `json:"is_gameconsole"`
IsMediaPlayer bool `json:"is_mediaplayer"`
IsMobile bool `json:"is_mobile"`
IsSmartTV bool `json:"is_smarttv"`
IsTablet bool `json:"is_tablet"`
IsTVPlayer bool `json:"is_tvplayer"`
IsDesktop bool `json:"is_desktop"`
IsTouchscreen bool `json:"is_touchscreen"`
} `json:"device"`
}

func Lookup(userAgent string) (Device, error) {
var d Device

raw, err := fastly.DeviceLookup(userAgent)
if err != nil {
status, ok := fastly.IsFastlyError(err)
switch {
case ok && status == fastly.FastlyStatusNone:
return d, ErrDeviceNotFound
case ok:
return d, fmt.Errorf("%w (%s)", ErrUnexpected, status)
default:
return d, err
}
}

if err := json.Unmarshal(raw, &d.info); err != nil {
return d, err
}

return d, nil
}

// Name returns the name of the client device.
func (d *Device) Name() string {
return d.info.Device.Name
}

// Brand returns the brand of the client device, possibly different from
// the manufacturer of that device.
func (d *Device) Brand() string {
return d.info.Device.Brand
}

// Model returns the model of the client device.
func (d *Device) Model() string {
return d.info.Device.Model
}

// HWType returns a string representation of the primary client platform
// hardware. The most commonly used device types are also identified
// via boolean variables. Because a device may have multiple device
// types and this variable only has the primary type, we recommend using
// the boolean variables for logic and using this string representation
// for logging.
func (d *Device) HWType() string {
return d.info.Device.HWType
}

// IsEReader returns true if the client device is a reading device (like
// a Kindle).
func (d *Device) IsEReader() bool {
return d.info.Device.IsEReader
}

// IsGameConsole returns true if the client device is a video game
// console (like a PlayStation or Xbox).
func (d *Device) IsGameConsole() bool {
return d.info.Device.IsGameConsole
}

// IsMediaPlayer returns true if the client device is a media player
// (like Blu-ray players, iPod devices, and smart speakers such as
// Amazon Echo).
func (d *Device) IsMediaPlayer() bool {
return d.info.Device.IsMediaPlayer
}

// IsMobile returns true if the client device is a mobile phone.
func (d *Device) IsMobile() bool {
return d.info.Device.IsMobile
}

// IsSmartTV returns true if the client device is a smart TV.
func (d *Device) IsSmartTV() bool {
return d.info.Device.IsSmartTV
}

// IsTablet returns true if the client device is a tablet (like an
// iPad).
func (d *Device) IsTablet() bool {
return d.info.Device.IsTablet
}

// IsTVPlayer returns true if the client device is a set-top box or
// other TV player (like a Roku or Apple TV).
func (d *Device) IsTVPlayer() bool {
return d.info.Device.IsTVPlayer
}

// IsDesktop returns true if the client device is a desktop web browser.
func (d *Device) IsDesktop() bool {
return d.info.Device.IsDesktop
}

// IsTouchscreen returns true if the client device's screen is touch
// sensitive.
func (d *Device) IsTouchscreen() bool {
return d.info.Device.IsTouchscreen
}
21 changes: 21 additions & 0 deletions integration_tests/device_detection/fastly.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# This file describes a Fastly Compute package. To learn more visit:
# https://developer.fastly.com/reference/fastly-toml/

authors = ["oss@fastly.com"]
description = ""
language = "go"
manifest_version = 2
name = "geolocation"
service_id = ""


[local_server]
[local_server.device_detection]
format = "inline-toml"

[local_server.device_detection.user_agents]
[local_server.device_detection.user_agents."Mozilla/5.0 (X11; Linux x86_64; rv:10.0) Gecko/20100101 Firefox/10.0 [FBAN/FBIOS;FBAV/8.0.0.28.18;FBBV/1665515;FBDV/iPhone4,1;FBMD/iPhone;FBSN/iPhone OS;FBSV/7.0.4;FBSS/2; FBCR/Telekom.de;FBID/phone;FBLC/de_DE;FBOP/5]"]
device = {name = "iPhone", brand = "Apple", model = "iPhone4,1", hwtype = "Mobile Phone", is_ereader = false, is_gameconsole = false, is_mediaplayer = false, is_mobile = true, is_smarttv = false, is_tablet = false, is_tvplayer = false, is_desktop = false, is_touchscreen = true}

[local_server.device_detection.user_agents."ghosts-app/1.0.2.1 (ASUSTeK COMPUTER INC.; X550CC; Windows 8 (X86); en)"]
device = {name = "Asus TeK", brand = "Asus", model = "TeK", is_desktop = false}
99 changes: 99 additions & 0 deletions integration_tests/device_detection/main_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
//go:build ((tinygo.wasm && wasi) || wasip1) && !nofastlyhostcalls

// Copyright 2023 Fastly, Inc.

package main

import (
"context"
"fmt"
"testing"

"github.com/fastly/compute-sdk-go/device"
"github.com/fastly/compute-sdk-go/fsthttp"
"github.com/fastly/compute-sdk-go/fsttest"
)

func assert[T comparable](res fsthttp.ResponseWriter, field string, got, want T) {
if got != want {
fsthttp.Error(res, fmt.Sprintf("%s: got %v, want %v", field, got, want), fsthttp.StatusInternalServerError)
}
}

func TestDeviceDetection(t *testing.T) {
handler := func(ctx context.Context, res fsthttp.ResponseWriter, req *fsthttp.Request) {
d, err := device.Lookup(req.Header.Get("User-Agent"))

switch req.URL.Path {
case "/iPhone":
if err != nil {
fsthttp.Error(res, err.Error(), fsthttp.StatusInternalServerError)
return
}

assert(res, "Name", d.Name(), "iPhone")
assert(res, "Brand", d.Brand(), "Apple")
assert(res, "Model", d.Model(), "iPhone4,1")
assert(res, "HWType", d.HWType(), "Mobile Phone")
assert(res, "IsMobile", d.IsMobile(), true)
assert(res, "IsTouchscreen", d.IsTouchscreen(), true)

case "/AsusTeK":
if err != nil {
fsthttp.Error(res, err.Error(), fsthttp.StatusInternalServerError)
return
}

assert(res, "Name", d.Name(), "Asus TeK")
assert(res, "Brand", d.Brand(), "Asus")
assert(res, "Model", d.Model(), "TeK")

case "/unknown":
if err != device.ErrDeviceNotFound {
fsthttp.Error(res, err.Error(), fsthttp.StatusInternalServerError)
return
}

default:
fsthttp.Error(res, "not found", fsthttp.StatusNotFound)
}
}

testcases := []struct {
name string
userAgent string
}{
{
name: "iPhone",
userAgent: "Mozilla/5.0 (X11; Linux x86_64; rv:10.0) Gecko/20100101 Firefox/10.0 [FBAN/FBIOS;FBAV/8.0.0.28.18;FBBV/1665515;FBDV/iPhone4,1;FBMD/iPhone;FBSN/iPhone OS;FBSV/7.0.4;FBSS/2; FBCR/Telekom.de;FBID/phone;FBLC/de_DE;FBOP/5]",
},

{
name: "AsusTeK",
userAgent: "ghosts-app/1.0.2.1 (ASUSTeK COMPUTER INC.; X550CC; Windows 8 (X86); en)",
},

{
name: "unknown",
userAgent: "whoopty doopty doo",
},
}

for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
r, err := fsthttp.NewRequest("GET", "/"+tc.name, nil)
if err != nil {
t.Fatal(err)
}
r.Header.Set("User-Agent", tc.userAgent)
w := fsttest.NewRecorder()

handler(context.Background(), w, r)

if got, want := w.Code, fsthttp.StatusOK; got != want {
t.Errorf("got %v, want %v", got, want)
t.Error(w.Body.String())
}
})
}
}
52 changes: 52 additions & 0 deletions internal/abi/fastly/hostcalls_guest.go
Original file line number Diff line number Diff line change
Expand Up @@ -3387,3 +3387,55 @@ func PurgeSurrogateKey(surrogateKey string, opts PurgeOptions) error {
prim.ToPointer(&opts.opts),
).toError()
}

// witx:
//
// (module $fastly_device_detection
// (@interface func (export "lookup")
// (param $user_agent string)
//
// (param $buf (@witx pointer (@witx char8)))
// (param $buf_len (@witx usize))
// (param $nwritten_out (@witx pointer (@witx usize)))
// (result $err (expected (error $fastly_status)))
// )
// )
//
//go:wasmimport fastly_device_detection lookup
//go:noescape
func fastlyDeviceDetectionLookup(
userAgentData prim.Pointer[prim.U8], userAgentLen prim.Usize,
buf prim.Pointer[prim.Char8],
bufLen prim.Usize,
nWritten prim.Pointer[prim.Usize],
) FastlyStatus

func DeviceLookup(userAgent string) ([]byte, error) {
buf := prim.NewWriteBuffer(defaultBufferLen)

userAgentBuffer := prim.NewReadBufferFromString(userAgent).Wstring()

status := fastlyDeviceDetectionLookup(
userAgentBuffer.Data, userAgentBuffer.Len,
prim.ToPointer(buf.Char8Pointer()),
buf.Cap(),
prim.ToPointer(buf.NPointer()),
)
if status == FastlyStatusBufLen {
// The buffer was too small, but it'll tell us how big it will
// need to be in order to fit the content.
buf = prim.NewWriteBuffer(int(buf.NValue()))

status = fastlyDeviceDetectionLookup(
userAgentBuffer.Data, userAgentBuffer.Len,
prim.ToPointer(buf.Char8Pointer()),
buf.Cap(),
prim.ToPointer(buf.NPointer()),
)
}
if err := status.toError(); err != nil {
return nil, err
}

return buf.AsBytes(), nil
}
4 changes: 4 additions & 0 deletions internal/abi/fastly/hostcalls_noguest.go
Original file line number Diff line number Diff line change
Expand Up @@ -468,3 +468,7 @@ func (o *PurgeOptions) SoftPurge(v bool) error {
func PurgeSurrogateKey(surrogateKey string, opts PurgeOptions) error {
return fmt.Errorf("not implemented")
}

func DeviceLookup(userAgent string) ([]byte, error) {
return nil, fmt.Errorf("not implemented")
}

0 comments on commit bc56fc2

Please sign in to comment.