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 +12/−0
- LICENSE +13/−0
- README.lhs +64/−0
- README.md +64/−0
- Setup.hs +2/−0
- mixpanel-client.cabal +103/−0
- src/MixPanel.hs +91/−0
- src/MixPanel/API.hs +38/−0
- src/MixPanel/Types/Core.hs +44/−0
- src/MixPanel/Types/EngageData.hs +89/−0
- src/MixPanel/Types/TrackData.hs +57/−0
- test/Main.hs +11/−0
- test/Spec.hs +1/−0
- test/TypesSpec.hs +23/−0
+ 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+++[](https://travis-ci.com/domenkozar/mixpanel-client)+[](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+++[](https://travis-ci.com/domenkozar/mixpanel-client)+[](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}"