diff --git a/elm.json b/elm.json index 36ed45a..7fe39a5 100644 --- a/elm.json +++ b/elm.json @@ -16,6 +16,7 @@ "elm/random": "1.0.0 <= v < 2.0.0", "elm/time": "1.0.0 <= v < 2.0.0", "elm/url": "1.0.0 <= v < 2.0.0", + "robinheghan/murmur3": "1.0.0 <= v < 2.0.0", "rtfeldman/elm-iso8601-date-strings": "1.1.4 <= v < 2.0.0" }, "test-dependencies": {} diff --git a/example/Example.elm b/example/Example.elm index 68b190e..dd5ee52 100644 --- a/example/Example.elm +++ b/example/Example.elm @@ -17,8 +17,8 @@ token = Debug.todo "00000000000000000000000000000000" -trackJsWithStartTime : Maybe Time.Posix -> TrackJS -trackJsWithStartTime time = +trackJSReporter : Maybe Time.Posix -> TrackJS +trackJSReporter time = let context = TrackJS.emptyContext @@ -43,7 +43,7 @@ type alias Model = initialModel : Model initialModel = { report = "Example report" - , trackJS = trackJsWithStartTime Nothing + , trackJS = trackJSReporter Nothing } @@ -62,7 +62,7 @@ update : Msg -> Model -> ( Model, Cmd Msg ) update msg model = case msg of StartTime time -> - ( { model | trackJS = trackJsWithStartTime (Just time) }, Cmd.none ) + ( { model | trackJS = trackJSReporter (Just time) }, Cmd.none ) SetText text -> ( { model | report = text }, Cmd.none ) diff --git a/example/elm.json b/example/elm.json index e16e625..7c9b6c4 100644 --- a/example/elm.json +++ b/example/elm.json @@ -16,6 +16,7 @@ "elm/random": "1.0.0", "elm/time": "1.0.0", "elm/url": "1.0.0", + "robinheghan/murmur3": "1.0.0", "rtfeldman/elm-iso8601-date-strings": "1.1.4" }, "indirect": { diff --git a/src/Murmur3.elm b/src/Murmur3.elm deleted file mode 100644 index 6a1010d..0000000 --- a/src/Murmur3.elm +++ /dev/null @@ -1,134 +0,0 @@ -module Murmur3 exposing (hashString) - -{-| Murmur 3 hash function for hashing strings. -Copied over from Skinney/murmur3. - -LICENSE -The MIT License (MIT) -Copyright (c) 2016 Robin Heggelund Hansen - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -@docs hashString - --} - -import Bitwise as Bit - - -type alias HashData = - { shift : Int - , seed : Int - , hash : Int - , charsProcessed : Int - } - - -c1 : Int -c1 = - 0xCC9E2D51 - - -c2 : Int -c2 = - 0x1B873593 - - -{-| Takes a seed and a string. Produces a hash (integer). -Given the same seed and string, it will always produce the same hash. -hashString 1234 "Turn me into a hash" == 4138100590 --} -hashString : Int -> String -> Int -hashString seed str = - str - |> String.foldl hashFold (HashData 0 seed 0 0) - |> finalize - - -hashFold : Char -> HashData -> HashData -hashFold c data = - let - res = - Char.toCode c - |> Bit.and 0xFF - |> Bit.shiftLeftBy data.shift - |> Bit.or data.hash - in - -- Using case-of instead of == avoids costly .cmp check - case data.shift of - 24 -> - { shift = 0 - , seed = mix data.seed res - , hash = 0 - , charsProcessed = data.charsProcessed + 1 - } - - _ -> - { shift = data.shift + 8 - , seed = data.seed - , hash = res - , charsProcessed = data.charsProcessed + 1 - } - - -mix : Int -> Int -> Int -mix h1 k1 = - (k1 - |> multiplyBy c1 - |> rotlBy 15 - |> multiplyBy c2 - |> Bit.xor h1 - |> rotlBy 13 - |> multiplyBy 5 - ) - + 0xE6546B64 - - -finalize : HashData -> Int -finalize data = - let - acc = - if data.hash /= 0 then - data.hash - |> multiplyBy c1 - |> rotlBy 15 - |> multiplyBy c2 - |> Bit.xor data.seed - - else - data.seed - - h0 = - Bit.xor acc data.charsProcessed - - h1 = - Bit.xor h0 (Bit.shiftRightZfBy 16 h0) - |> multiplyBy 0x85EBCA6B - - h2 = - Bit.xor h1 (Bit.shiftRightZfBy 13 h1) - |> multiplyBy 0xC2B2AE35 - in - Bit.xor h2 (Bit.shiftRightZfBy 16 h2) - |> Bit.shiftRightZfBy 0 - - -{-| 32-bit multiplication --} -multiplyBy : Int -> Int -> Int -multiplyBy b a = - (Bit.and a 0xFFFF * b) + Bit.shiftLeftBy 16 (Bit.and (Bit.shiftRightZfBy 16 a * b) 0xFFFF) - - -{-| Given a 32bit int and an int representing a number of bit positions, -returns the 32bit int rotated left by that number of positions. --} -rotlBy : Int -> Int -> Int -rotlBy b a = - Bit.or - (Bit.shiftLeftBy b a) - (Bit.shiftRightZfBy (32 - b) a) diff --git a/src/TrackJS.elm b/src/TrackJS.elm index 8df0f1f..08c84b4 100644 --- a/src/TrackJS.elm +++ b/src/TrackJS.elm @@ -51,7 +51,7 @@ Create one using [`reporter`](#reporter). -} type alias TrackJS = - { report : Report -> Dict String String -> Task Http.Error Uuid + { report : Report -> Dict String String -> Task Http.Error () } @@ -232,29 +232,25 @@ Arguments: - `Dict String String` - arbitrary metadata key-value pairs If the message was successfully sent to TrackJS, the [`Task`](http://package.elm-lang.org/packages/elm-lang/core/latest/Task#Task) -succeeds with the [`Uuid`](http://package.elm-lang.org/packages/danyx23/elm-uuid/latest/Uuid#Uuid) -it generated and sent to TrackJS to identify the message. Otherwise it fails -with the [`Http.Error`](http://package.elm-lang.org/packages/elm-lang/http/latest/Http#Error) +succeeds. +Otherwise it fails with the +[`Http.Error`](http://package.elm-lang.org/packages/elm-lang/http/latest/Http#Error) responsible (however note that [TrackJS always responds](https://docs.trackjs.com/data-api/capture/) with `200 OK` or `202 ACCEPTED`). -} -send : Token -> CodeVersion -> Application -> Context -> Report -> Dict String String -> Task Http.Error Uuid +send : Token -> CodeVersion -> Application -> Context -> Report -> Dict String String -> Task Http.Error () send vtoken vcodeVersion vapplication context report metadata = Time.now |> Task.andThen (sendWithTime vtoken vcodeVersion vapplication context report metadata) -sendWithTime : Token -> CodeVersion -> Application -> Context -> Report -> Dict String String -> Posix -> Task Http.Error Uuid +sendWithTime : Token -> CodeVersion -> Application -> Context -> Report -> Dict String String -> Posix -> Task Http.Error () sendWithTime (Token vtoken) vcodeVersion vapplication context report metadata time = let - uuid : Uuid - uuid = - uuidFrom (Token vtoken) vapplication report.message metadata time - body : Http.Body body = - toJsonBody (Token vtoken) vcodeVersion vapplication context report uuid metadata time + toJsonBody (Token vtoken) vcodeVersion vapplication context report metadata time in -- POST https://capture.trackjs.com/capture?token={TOKEN}&v={AGENT_VERSION} { method = "POST" @@ -266,52 +262,84 @@ sendWithTime (Token vtoken) vcodeVersion vapplication context report metadata ti , Url.Builder.string "v" TrackJS.Internal.version ] , body = body - , resolver = Http.stringResolver (\_ -> Ok ()) -- TODO + , resolver = + Http.stringResolver + (\response -> + case response of + Http.BadUrl_ url -> + Err <| Http.BadUrl url + + Http.Timeout_ -> + Err Http.Timeout + + Http.NetworkError_ -> + Err Http.NetworkError + + Http.BadStatus_ meta _ -> + Err <| Http.BadStatus meta.statusCode + + Http.GoodStatus_ _ _ -> + Ok () + ) , timeout = Nothing } |> Http.task - |> Task.map (\() -> uuid) - - -{-| Using the current system time as a random number seed generator, generate a -UUID. -We could theoretically generate the same UUID twice if we tried to send -two messages in extremely rapid succession. To guard against this, we -incorporate the contents of the message in the random number seed so that the -only way we could expect the same UUID is if we were sending a duplicate -message. +{-| Tries to generate a correlationId that might be unique to the current +"user", in the vague sense of a browser tab session, by using various bits of +information. -} -uuidFrom : Token -> Application -> String -> Dict String String -> Posix -> Uuid -uuidFrom (Token vtoken) (Application vapplication) message metadata time = +correlationIdFrom : CodeVersion -> Context -> Report -> Posix -> Uuid +correlationIdFrom (CodeVersion vcodeVersion) context report time = let - ms = - Time.posixToMillis time + ( itemsToHash, seed ) = + if context == emptyContext then + -- If the context is empty, there's no easy way to identify a + -- user, so use the code version and report as hash items, + -- with the current time as seed, to generate a semi-unique + -- hash + ( [ Encode.string vcodeVersion + , Encode.string report.message + , Encode.string report.url + , Encode.string <| Maybe.withDefault "" report.stackTrace + ] + , Time.posixToMillis time + ) + + else + -- Otherwise use the context information in the hash items, + -- which should be relatively stable for a user, and the user's + -- `startTime` as a seed (if possible) to try and correlate + -- errors + ( [ Encode.string vcodeVersion + , Encode.string context.sessionId + , Encode.string context.userId + , Encode.string context.originalUrl + , Encode.string context.referrer + , Encode.string context.userAgent + , Encode.int context.viewport.h + , Encode.int context.viewport.w + ] + , Maybe.withDefault 0 <| Maybe.map Time.posixToMillis context.startTime + ) hash : Int hash = - [ Encode.string message - , Encode.string vtoken - , Encode.string vapplication - , Encode.dict identity Encode.string metadata - ] + itemsToHash |> Encode.list identity |> Encode.encode 0 - |> Murmur3.hashString ms - - combinedSeed = - Bitwise.xor (floor (ms |> toFloat)) hash + |> Murmur3.hashString seed in - Random.initialSeed combinedSeed + Random.initialSeed hash |> Random.step uuidGenerator |> Tuple.first {-| See for schema -} -toJsonBody : Token -> CodeVersion -> Application -> Context -> Report -> Uuid -> Dict String String -> Posix -> Http.Body -toJsonBody (Token vtoken) (CodeVersion vcodeVersion) (Application vapplication) context report uuid metadata time = +toJsonBody : Token -> CodeVersion -> Application -> Context -> Report -> Dict String String -> Posix -> Http.Body +toJsonBody (Token vtoken) (CodeVersion vcodeVersion) (Application vapplication) context report metadata time = -- The source platform of the capture. Typically "browser" or "node". {String} [ ( "agentPlatform", Encode.string "browser-elm" ) @@ -332,9 +360,10 @@ toJsonBody (Token vtoken) (CodeVersion vcodeVersion) (Application vapplication) [ ( "application", Encode.string vapplication ) -- Auto-generated ID for matching visitor to multiple errors. {String} - -- FIXME seems like this should not be the error UUID but per user session - -- FIXME maybe generate UUID on less info so it groups errors? - , ( "correlationId", Uuid.encode uuid ) + , ( "correlationId" + , Uuid.encode <| + correlationIdFrom (CodeVersion vcodeVersion) context report time + ) -- Customer-generated Id for the current browser session. {String} , ( "sessionId", Encode.string context.sessionId )