packages feed

mixpanel-client (empty) → 0.1.0.0

raw patch · 14 files changed

+612/−0 lines, 14 filesdep +aesondep +basedep +base64-bytestringsetup-changed

Dependencies added: aeson, base, base64-bytestring, bytestring, hspec, http-client, http-client-tls, mixpanel-client, servant, servant-client, string-conv, text, time

Files

+ ChangeLog.md view
@@ -0,0 +1,12 @@+# Changelog++All notable changes to this project will be documented in this file.++The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)+and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).++## [Unreleased]++## [0.1.1] - 2018-08-25++- Intial release @domenkozar
+ LICENSE view
@@ -0,0 +1,13 @@+Copyright 2018 Domen Kožar++Licensed under the Apache License, Version 2.0 (the "License");+you may not use this file except in compliance with the License.+You may obtain a copy of the License at++    http://www.apache.org/licenses/LICENSE-2.0++Unless required by applicable law or agreed to in writing, software+distributed under the License is distributed on an "AS IS" BASIS,+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+See the License for the specific language governing permissions and+limitations under the License.
+ README.lhs view
@@ -0,0 +1,64 @@+# MixPanel client for Haskell+++[![Build Status](https://travis-ci.com/domenkozar/mixpanel-client.svg?branch=master)](https://travis-ci.com/domenkozar/mixpanel-client)+[![Hackage](https://img.shields.io/hackage/v/mixpanel-client.svg)](https://hackage.haskell.org/package/mixpanel-client)+++Implements major features of [MixPanel HTTP API](https://mixpanel.com/help/reference/http):++- [x] track+- [x] alias+- [x] engage+- [ ] import+- [ ] export++## Getting started++```haskell+import Data.Aeson      ( (.=) )+import Data.Time.Clock ( getCurrentTime )+import GHC.Exts        ( fromList )+import MixPanel        ( Operation(Set), engage, track, alias+                       , AuthToken(..), mkEnv+                       -- reexports+                       , newManager, tlsManagerSettings)+++main :: IO ()+main = do+  -- setup+  manager <- newManager tlsManagerSettings+  let env = mkEnv (AuthToken "foobar") manager++  -- track a simple event+  Right () <- track env "Played Video" mempty++  -- track an event with extra properties+  Right () <- track env "Played Video Unique per user"+    $ fromList [ "distinct_id" .= ("generated-id" :: String)+               , "customProperty" .= True ]++  -- alias the user+  Right () <- alias env "generated-id" "user@example.com"++  -- profile engagement+  now <- getCurrentTime+  Right () <- engage env "user@example.com"+    $ Set (fromList [ "$created" .= now])++  putStrLn "All good!"+```++## Design questions++a) Why does it use `Object/Array` intemediate values from `Data.Aeson`?+++## TODO++- batch requests+- expose extra url query paramerers+- engage special properties more type safe?+- filter out null values in requests to mixpanel+- api support for tracking revenue
+ README.md view
@@ -0,0 +1,64 @@+# MixPanel client for Haskell+++[![Build Status](https://travis-ci.com/domenkozar/mixpanel-client.svg?branch=master)](https://travis-ci.com/domenkozar/mixpanel-client)+[![Hackage](https://img.shields.io/hackage/v/mixpanel-client.svg)](https://hackage.haskell.org/package/mixpanel-client)+++Implements major features of [MixPanel HTTP API](https://mixpanel.com/help/reference/http):++- [x] track+- [x] alias+- [x] engage+- [ ] import+- [ ] export++## Getting started++```haskell+import Data.Aeson      ( (.=) )+import Data.Time.Clock ( getCurrentTime )+import GHC.Exts        ( fromList )+import MixPanel        ( Operation(Set), engage, track, alias+                       , AuthToken(..), mkEnv+                       -- reexports+                       , newManager, tlsManagerSettings)+++main :: IO ()+main = do+  -- setup+  manager <- newManager tlsManagerSettings+  let env = mkEnv (AuthToken "foobar") manager++  -- track a simple event+  Right () <- track env "Played Video" mempty++  -- track an event with extra properties+  Right () <- track env "Played Video Unique per user"+    $ fromList [ "distinct_id" .= ("generated-id" :: String)+               , "customProperty" .= True ]++  -- alias the user+  Right () <- alias env "generated-id" "user@example.com"++  -- profile engagement+  now <- getCurrentTime+  Right () <- engage env "user@example.com"+    $ Set (fromList [ "$created" .= now])++  putStrLn "All good!"+```++## Design questions++a) Why does it use `Object/Array` intemediate values from `Data.Aeson`?+++## TODO++- batch requests+- expose extra url query paramerers+- engage special properties more type safe?+- filter out null values in requests to mixpanel+- api support for tracking revenue
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ mixpanel-client.cabal view
@@ -0,0 +1,103 @@+-- This file has been generated from package.yaml by hpack version 0.28.2.+--+-- see: https://github.com/sol/hpack+--+-- hash: 491a6eeca6a6f94f2fbd7939dffe3bdf350c15ec0a7287c81ade08416e93c4a6++name:           mixpanel-client+version:        0.1.0.0+synopsis:       Mixpanel client+homepage:       https://github.com/domenkozar/mixpanel-client#readme+bug-reports:    https://github.com/domenkozar/mixpanel-client/issues+author:         Domen Kožar+maintainer:     domen@enlambda.com+copyright:      2018 Domen Kožar+license:        Apache-2.0+license-file:   LICENSE+build-type:     Simple+cabal-version:  >= 1.10+extra-source-files:+    ChangeLog.md+    README.md++source-repository head+  type: git+  location: https://github.com/domenkozar/mixpanel-client++library+  exposed-modules:+      MixPanel+      MixPanel.API+      MixPanel.Types.Core+      MixPanel.Types.EngageData+      MixPanel.Types.TrackData+  other-modules:+      Paths_mixpanel_client+  hs-source-dirs:+      src+  default-extensions: OverloadedStrings LambdaCase DeriveGeneric DeriveFunctor DeriveAnyClass RecordWildCards+  ghc-options: -Wall -fwarn-tabs -fwarn-unused-imports -fwarn-missing-signatures -fwarn-name-shadowing -fwarn-incomplete-patterns+  build-depends:+      aeson+    , base >=4.7 && <5+    , base64-bytestring+    , bytestring+    , http-client+    , http-client-tls+    , servant >=0.14.1+    , servant-client+    , string-conv+    , text+    , time+  default-language: Haskell2010++test-suite mixpanel-test+  type: exitcode-stdio-1.0+  main-is: Main.hs+  other-modules:+      Spec+      TypesSpec+      Paths_mixpanel_client+  hs-source-dirs:+      test+  default-extensions: OverloadedStrings LambdaCase DeriveGeneric DeriveFunctor DeriveAnyClass RecordWildCards+  ghc-options: -Wall -fwarn-tabs -fwarn-unused-imports -fwarn-missing-signatures -fwarn-name-shadowing -fwarn-incomplete-patterns+  build-depends:+      aeson+    , base >=4.7 && <5+    , base64-bytestring+    , bytestring+    , hspec+    , http-client+    , http-client-tls+    , mixpanel-client+    , servant >=0.14.1+    , servant-client+    , string-conv+    , text+    , time+  default-language: Haskell2010+  build-tool-depends: hspec-discover:hspec-discover++test-suite readme+  type: exitcode-stdio-1.0+  main-is: README.lhs+  other-modules:+      Paths_mixpanel_client+  default-extensions: OverloadedStrings LambdaCase DeriveGeneric DeriveFunctor DeriveAnyClass RecordWildCards+  ghc-options: -Wall -fwarn-tabs -fwarn-unused-imports -fwarn-missing-signatures -fwarn-name-shadowing -fwarn-incomplete-patterns -pgmL markdown-unlit+  build-depends:+      aeson+    , base+    , base64-bytestring+    , bytestring+    , http-client+    , http-client-tls+    , mixpanel-client+    , servant >=0.14.1+    , servant-client+    , string-conv+    , text+    , time+  default-language: Haskell2010+  build-tool-depends: markdown-unlit:markdown-unlit
+ src/MixPanel.hs view
@@ -0,0 +1,91 @@+module MixPanel+  ( MixPanelError(..)+  , AuthToken(..)+  , Operation(..)+  , DistinctId+  -- Setup+  , mkEnv+  , Env+  -- mixpanel api calls+  , track+  , alias+  , engage+  -- HTTP manager reexports+  , tlsManagerSettings+  , HTTP.newManager+  ) where++import           Data.Aeson                     ( Object, (.=) )+import           Data.Text                      ( Text )+import           GHC.Exts                       ( fromList )+import qualified Network.HTTP.Client           as HTTP+import           Network.HTTP.Client.TLS        ( tlsManagerSettings )+import           Servant.API             hiding ( URI )+import           Servant.Client++import           MixPanel.API                   ( api )+import           MixPanel.Types.Core            ( DidSucceed(..)+                                                , Toggle(..)+                                                , AuthToken(..)+                                                )+import           MixPanel.Types.TrackData       ( TrackData(..)+                                                , mkProperties+                                                )+import           MixPanel.Types.EngageData      ( EngageData, DistinctId, Operation(..), mkEngageData )+++host :: BaseUrl+host = BaseUrl Https "api.mixpanel.com" 443 ""++data MixPanelError+  = ServantError ServantError+  | Error Text++data Env = Env+  { authtoken :: AuthToken+  , httpManager :: HTTP.Manager+  , clientEnv :: ClientEnv+  }++mkEnv :: AuthToken -> HTTP.Manager -> Env+mkEnv authtoken httpManager = Env {..}+  where+    clientEnv = mkClientEnv httpManager host++++trackC :: TrackData -> Maybe Toggle -> Maybe Text -> Maybe Toggle -> Maybe Text -> Maybe Toggle -> ClientM DidSucceed+engageC :: EngageData -> Maybe Text -> Maybe Text -> Maybe Toggle -> ClientM DidSucceed+trackC :<|> engageC = client api++trackC' :: TrackData -> ClientM DidSucceed+trackC' trackdata = trackC trackdata Nothing Nothing Nothing Nothing (Just On)++engageC' :: EngageData -> ClientM DidSucceed+engageC' engagedata =+  engageC engagedata Nothing Nothing (Just On)++runMixPanel :: ClientEnv -> ClientM DidSucceed -> IO (Either MixPanelError ())+runMixPanel clientEnv comp = do+  result <- runClientM comp clientEnv+  return $ case result of+    Left err -> Left $ ServantError err+    Right (Fail err) -> Left $ Error err+    Right Success -> Right ()++track :: Env -> Text -> Object -> IO (Either MixPanelError ())+track Env{..} event props =+  runMixPanel clientEnv $ trackC' $ TrackData event $ mkProperties authtoken props++-- | Renames distinct id into alias id+alias :: Env -> DistinctId -> Text -> IO (Either MixPanelError ())+alias Env{..} distinctId aliasId =+   runMixPanel clientEnv $ trackC' $ TrackData "$create_alias" $ mkProperties authtoken props+    where+      props :: Object+      props = fromList [ "alias" .= aliasId+                       , "distinct_id" .= distinctId]++engage :: Env -> DistinctId -> Operation -> IO (Either MixPanelError ())+engage Env{..} distinctid operation =+  runMixPanel clientEnv $ engageC' $ mkEngageData authtoken distinctid operation
+ src/MixPanel/API.hs view
@@ -0,0 +1,38 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE TypeOperators #-}+{- As per https://mixpanel.com/help/reference/http+-}+module MixPanel.API+  ( API+  , api+  ) where++import           Data.Text                      ( Text )+import           Data.Proxy                     ( Proxy(..) )+import           Servant.API             hiding ( URI )++import           MixPanel.Types.Core            ( DidSucceed, Toggle )+import           MixPanel.Types.TrackData       ( TrackData )+import           MixPanel.Types.EngageData      ( EngageData )+++type Track = "track"+          :> QueryParam' '[Required] "data" TrackData+          :> QueryParam "ip" Toggle+          :> QueryParam "redirect" Text -- URI+          :> QueryParam "img" Toggle+          :> QueryParam "callback" Text+          :> QueryParam "verbose" Toggle+          :> Get '[JSON] DidSucceed++type Engage = "engage"+           :> QueryParam' '[Required] "data" EngageData+           :> QueryParam "redirect" Text -- URI+           :> QueryParam "callback" Text+           :> QueryParam "verbose" Toggle+           :> Get '[JSON] DidSucceed++type API = Track :<|> Engage++api :: Proxy API+api = Proxy
+ src/MixPanel/Types/Core.hs view
@@ -0,0 +1,44 @@+module MixPanel.Types.Core+  ( AuthToken(..)+  , Toggle(..)+  , DidSucceed(..)+  ) where++import           Data.Aeson                     ( ToJSON+                                                , FromJSON+                                                , parseJSON+                                                , toJSON+                                                , withObject+                                                , (.:)+                                                )+import           Data.Text                      ( Text )+import           GHC.Generics                   ( Generic )+import           Servant.API++newtype AuthToken = AuthToken Text+  deriving (Generic, ToJSON)++-- | MixPanel API uses boolean logic with 1 or 0,+-- | so we have a special type for it+data Toggle = On | Off+  deriving (Show)++instance ToJSON Toggle where+  toJSON On = "1"+  toJSON Off = "0"++instance ToHttpApiData Toggle where+  toUrlPiece On = "1"+  toUrlPiece Off = "0"+++data DidSucceed = Success | Fail Text+  deriving (Show)++instance FromJSON DidSucceed where+  parseJSON = withObject "success or failure" $ \o -> do+    status <- o .: "status"+    case (status :: Int) of+      1 -> return Success+      0 -> Fail <$> o .: "error"+      _ -> fail ("unknown status: " ++ show status)
+ src/MixPanel/Types/EngageData.hs view
@@ -0,0 +1,89 @@+{-# LANGUAGE GADTs #-}+{-# LANGUAGE FlexibleContexts #-}+module MixPanel.Types.EngageData+  ( EngageData+  , DistinctId+  , mkEngageData+  , Operation(..)+  ) where++import           Data.Text                      ( Text )+import           Data.Aeson                     ( ToJSON+                                                , toJSON+                                                , Object+                                                , Value(..)+                                                , Array+                                                , encode+                                                , (.=)+                                                )+import qualified Data.ByteString.Base64.Lazy   as B64+import           Data.String.Conv               ( toS )+import           GHC.Exts                       ( fromList )+import           Servant.API++import           MixPanel.Types.Core            ( AuthToken )+++data Operation where+  Set :: Object -> Operation+  SetOnce :: Object -> Operation+  Add :: Object -> Operation+  Append :: Object -> Operation+  Union :: Object -> Operation+  Remove :: Object -> Operation+  Unset :: Array -> Operation+  Delete :: Operation++instance ToJSON Operation where+  toJSON (Set obj) = Object obj+  toJSON (SetOnce obj) = Object obj+  toJSON (Add obj) = Object obj+  toJSON (Append obj) = Object obj+  toJSON (Union obj) = Object obj+  toJSON (Remove obj) = Object obj+  toJSON (Unset obj) = Array obj+  toJSON Delete = ""++operationIdentifier :: Operation -> Text+operationIdentifier (Set _) = "$set"+operationIdentifier (SetOnce _) = "$set_once"+operationIdentifier (Add _) = "$add"+operationIdentifier (Append _) = "$append"+operationIdentifier (Union _) = "$union"+operationIdentifier (Remove _) = "$remove"+operationIdentifier (Unset _) = "$unset"+operationIdentifier Delete = "$delete"++type DistinctId = Text++data EngageData = EngageData+  { token :: AuthToken+  , distinctId :: DistinctId+  , ip :: Maybe Text+  , time :: Maybe Text+  , ignoreTime :: Maybe Text+  , operation :: Operation+  }++instance ToJSON EngageData where+  toJSON EngageData{..} = Object $ fromList+    [ "$token" .= token+    , "$distinct_id" .= distinctId+    , "$time" .= time+    , "$ignoreTime" .= ignoreTime+    , "$ip" .= ip+    , operationIdentifier operation .= toJSON operation+    ]++instance ToHttpApiData EngageData where+  toUrlPiece = toS . B64.encode . encode++mkEngageData :: AuthToken -> DistinctId -> Operation -> EngageData+mkEngageData token di op = EngageData+  { token = token+  , distinctId = di+  , ip = Nothing+  , time = Nothing+  , ignoreTime = Nothing+  , operation = op+  }
+ src/MixPanel/Types/TrackData.hs view
@@ -0,0 +1,57 @@+{-# LANGUAGE ExistentialQuantification #-}+module MixPanel.Types.TrackData+  ( TrackData(..)+  , Properties+  , mkProperties+  ) where++import           Data.Aeson                     ( ToJSON+                                                , toJSON+                                                , encode+                                                , Value(..)+                                                , Object+                                                , (.=)+                                                )+import           Data.Text                      ( Text )+import qualified Data.ByteString.Base64.Lazy   as B64+import           Data.Time.Clock.POSIX          ( POSIXTime )+import           GHC.Exts                       ( fromList )+import           GHC.Generics                   ( Generic )+import           Servant.API+import           Data.String.Conv               ( toS )+import           MixPanel.Types.Core            ( AuthToken )+++data TrackData = TrackData+  { event :: Text+  , properties :: Properties+  } deriving (Generic, ToJSON)+++instance ToHttpApiData TrackData where+  toUrlPiece = toS . B64.encode . encode++data Properties = Properties+  { token :: AuthToken+  , distinctId :: Maybe Text+  , time :: Maybe POSIXTime+  , ip :: Maybe Text+  , extraProperties :: Object+  }++instance ToJSON Properties where+  toJSON Properties{..} = Object $ extraProperties <> fromList+     [ "token" .= token+     , "distinct_id" .= distinctId+     , "time" .= time+     , "ip" .= ip+     ]++mkProperties :: AuthToken -> Object -> Properties+mkProperties token obj = Properties+  { token = token+  , distinctId = Nothing+  , time = Nothing+  , ip = Nothing+  , extraProperties = obj+  }
+ test/Main.hs view
@@ -0,0 +1,11 @@+module Main where++import Test.Hspec.Runner+import qualified Spec++main :: IO ()+main = hspecWith config Spec.spec+  where+    config = defaultConfig+      { configColorMode = ColorAlways+      }
+ test/Spec.hs view
@@ -0,0 +1,1 @@+{-# OPTIONS_GHC -F -pgmF hspec-discover -optF --module-name=Spec #-}
+ test/TypesSpec.hs view
@@ -0,0 +1,23 @@+module TypesSpec where++import Data.Aeson+import GHC.Exts   (fromList)+import Test.Hspec++import MixPanel.Types.Core+import MixPanel.Types.EngageData+import MixPanel.Types.TrackData+++spec :: Spec+spec = describe "TrackData/EngageData" $ do+  it "TrackData merges properties" $+    let+     td = TrackData "foobar" $ mkProperties (AuthToken "token123") props+     props :: Object+     props = fromList [ "distinct_id" .= ("bar" :: String) ]+    in encode td `shouldBe` "{\"event\":\"foobar\",\"properties\":{\"time\":null,\"token\":\"token123\",\"ip\":null,\"distinct_id\":\"bar\"}}"+  it "EngageData merges properties" $+    let+      ed = mkEngageData (AuthToken "foobar") "123" $ Set (fromList [ "customProperty" .= ("foobar" :: String)])+    in encode ed `shouldBe` "{\"$distinct_id\":\"123\",\"$set\":{\"customProperty\":\"foobar\"},\"$token\":\"foobar\",\"$ip\":null,\"$ignoreTime\":null,\"$time\":null}"